diff --git a/.gitignore b/.gitignore index a7f0dba83a..8f0076d4d2 100644 --- a/.gitignore +++ b/.gitignore @@ -51,7 +51,8 @@ Thumbs.db # Caches .angular -.nx +.nx/cache +.nx/workspace-data .sass-cache # Logs diff --git a/.verdaccio/config.yml b/.verdaccio/config.yml new file mode 100644 index 0000000000..f74420f2b9 --- /dev/null +++ b/.verdaccio/config.yml @@ -0,0 +1,28 @@ +# path to a directory with all packages +storage: ../tmp/local-registry/storage + +# a list of other known repositories we can talk to +uplinks: + npmjs: + url: https://registry.npmjs.org/ + maxage: 60m + +packages: + '**': + # give all users (including non-authenticated users) full access + # because it is a local registry + access: $all + publish: $all + unpublish: $all + + # if package is not available locally, proxy requests to npm registry + proxy: npmjs + +# log settings +log: + type: stdout + format: pretty + level: warn + +publish: + allow_offline: true # set offline to true to allow publish offline diff --git a/apps/humanatlas.io/src/app/resolvers/release-notes-content.resolver.ts b/apps/humanatlas.io/src/app/resolvers/release-notes-content.resolver.ts index 4a2193fb15..3905a41ed2 100644 --- a/apps/humanatlas.io/src/app/resolvers/release-notes-content.resolver.ts +++ b/apps/humanatlas.io/src/app/resolvers/release-notes-content.resolver.ts @@ -1,5 +1,5 @@ import { ResolveFn } from '@angular/router'; -import { joinWithSlash } from '@hra-ui/common/url'; +import { joinWithSlashes } from '@hra-ui/utils/paths'; import { ContentPageData, ContentPageDataSchema } from '@hra-ui/design-system/content-templates/content-page'; import { createYamlSpecResolver } from '@hra-ui/design-system/content-templates/resolvers'; @@ -11,7 +11,7 @@ import { createYamlSpecResolver } from '@hra-ui/design-system/content-templates/ export function createReleaseNotesContentResolver(baseUrl: string): ResolveFn { return (route, state) => { const version = route.params['version'] as string; - const url = joinWithSlash(baseUrl, `${version}.yaml`); + const url = joinWithSlashes(baseUrl, `${version}.yaml`); const resolver = createYamlSpecResolver(url, ContentPageDataSchema); return resolver(route, state); }; diff --git a/libs/cdk/injectors/src/lib/dispatch/dispatch.ts b/libs/cdk/injectors/src/lib/dispatch/dispatch.ts index abf6f8969b..6143316867 100644 --- a/libs/cdk/injectors/src/lib/dispatch/dispatch.ts +++ b/libs/cdk/injectors/src/lib/dispatch/dispatch.ts @@ -1,5 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { inject } from '@angular/core'; -import type { Any } from '@hra-ui/utils/types'; import { Store } from '@ngxs/store'; import { map, Observable } from 'rxjs'; @@ -19,7 +19,7 @@ function pipeActionInstance(action: A, obs$: Observable): Observable * @param boundArgs Initial bound arguments * @returns A factory function creating a new action on each call */ -function createActionFactory( +function createActionFactory( type: new (...args: [...BoundArgs, ...RestArgs]) => A, boundArgs: BoundArgs, ): (...args: RestArgs) => A { @@ -32,14 +32,14 @@ function createActionFactory * @param resultHandler Selects the output value from the action and the dispatch observable * @returns A new dispatch function taking user arguments, dispatches actions, and returns a value */ -function dispatchImpl( +function dispatchImpl( actionFactory: (...args: Args) => A, resultHandler: (action: A, obs$: Observable) => R, ): (...args: Args) => R { const store = inject(Store); return (...args) => { const action = actionFactory(...args); - const obs$ = store.dispatch(action as Any); + const obs$ = store.dispatch(action as any); return resultHandler(action, obs$); }; } @@ -51,7 +51,7 @@ function dispatchImpl( * @param boundArgs Bound arguments to the action constructor * @returns A function that dispatches an action on the store each time it is called */ -export function dispatch( +export function dispatch( type: new (...args: [...BoundArgs, ...DispatchArgs]) => A, ...boundArgs: BoundArgs ): (...args: DispatchArgs) => A { @@ -67,7 +67,7 @@ export function dispatch * @param boundArgs Bound arguments to the action constructor * @returns A function that dispatches an action on the store each time it is called */ -export function dispatch$( +export function dispatch$( type: new (...args: [...BoundArgs, ...DispatchArgs]) => A, ...boundArgs: BoundArgs ): (...args: DispatchArgs) => Observable { diff --git a/libs/cdk/injectors/src/lib/select/select-snapshot.ts b/libs/cdk/injectors/src/lib/select/select-snapshot.ts index e04565466f..824d389f78 100644 --- a/libs/cdk/injectors/src/lib/select/select-snapshot.ts +++ b/libs/cdk/injectors/src/lib/select/select-snapshot.ts @@ -1,18 +1,18 @@ -import type { Any, AnyFunction } from '@hra-ui/utils/types'; +/* eslint-disable @typescript-eslint/no-explicit-any */ import { select$, StateSelector } from './select'; import { SnapshotObserver } from './snapshot-observer'; /** Get remaining arguments after applying bound arguments */ -type RestArgs = F extends ( +type RestArgs any, BoundArgs extends any[]> = F extends ( ...args: [...BoundArgs, ...infer Rest] -) => Any +) => any ? Rest : never; /** Function type returned by {@link selectQuerySnapshot} */ -type SelectQuery = < +type SelectQuery any, BoundArgs extends any[]> = < Res = ReturnType, - Args extends Any[] = RestArgs + Args extends any[] = RestArgs, >( ...args: Args ) => Res; @@ -62,7 +62,7 @@ export function selectSnapshot(selector: StateSelector): () => T { * @param boundArgs Optional bound query arguments * @returns A snapshot function taking the same arguments as the query selector (excluding bound arguments) */ -export function selectQuerySnapshot Any, BoundArgs extends Any[]>( +export function selectQuerySnapshot any, BoundArgs extends any[]>( selector: StateSelector, ...boundArgs: BoundArgs ): SelectQuery { diff --git a/libs/cdk/injectors/src/lib/select/select.ts b/libs/cdk/injectors/src/lib/select/select.ts index 7d79fe8041..3c2b618c3b 100644 --- a/libs/cdk/injectors/src/lib/select/select.ts +++ b/libs/cdk/injectors/src/lib/select/select.ts @@ -1,11 +1,11 @@ import { ChangeDetectorRef, inject } from '@angular/core'; -import { Any } from '@hra-ui/utils/types'; import { StateToken, Store } from '@ngxs/store'; import { MonoTypeOperatorFunction, Observable, takeUntil, tap } from 'rxjs'; import { injectDestroy$ } from '../on-destroy/on-destroy'; /** Selector type for select style functions */ -export type StateSelector = ((...args: Any[]) => T) | StateToken; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type StateSelector = ((...args: any[]) => T) | StateToken; /** `select$` configuration options */ export interface SelectOptions { diff --git a/libs/cdk/package.json b/libs/cdk/package.json index abecac8777..50bb27a86f 100644 --- a/libs/cdk/package.json +++ b/libs/cdk/package.json @@ -5,7 +5,6 @@ "@angular/common": "^21.1.4", "@angular/core": "^21.1.4", "@ngxs/store": "^21.0.0", - "@hra-ui/utils": "*", "@angular/cdk": "^21.1.4", "rxjs": "^7.8.2", "@hra-ui/common": "1.1.0", diff --git a/libs/cdk/src/lib/link/link.directive.ts b/libs/cdk/src/lib/link/link.directive.ts index 807683a0b4..b9a5b89a9f 100644 --- a/libs/cdk/src/lib/link/link.directive.ts +++ b/libs/cdk/src/lib/link/link.directive.ts @@ -1,8 +1,15 @@ import { Directive, DoCheck, ElementRef, inject, Injector, Input } from '@angular/core'; import { ActivatedRoute, Params, QueryParamsHandling, UrlCreationOptions } from '@angular/router'; import { dispatch, selectQuerySnapshot } from '@hra-ui/cdk/injectors'; -import { EMPTY_LINK, LinkEntry, LinkRegistryActions, LinkRegistrySelectors, LinkType } from '@hra-ui/cdk/state'; -import { createExternalUrl, createInternalUrl } from '@hra-ui/utils'; +import { + createExternalUrl, + createInternalUrl, + EMPTY_LINK, + LinkEntry, + LinkRegistryActions, + LinkRegistrySelectors, + LinkType, +} from '@hra-ui/cdk/state'; /** Link Directive for routing */ @Directive({ diff --git a/libs/cdk/state/src/index.ts b/libs/cdk/state/src/index.ts index c441c863a3..13384ec8bf 100644 --- a/libs/cdk/state/src/index.ts +++ b/libs/cdk/state/src/index.ts @@ -1,7 +1,8 @@ export { assertUniqueActionType, registerActionType } from './actions/action-type-registry'; -export { Action, ActionGroup, ActionConstructor } from './actions/actions'; +export { Action, ActionConstructor, ActionGroup } from './actions/actions'; export * from './base-href'; export * from './link-registry'; +export * from './link-registry/url/create'; export * from './resource-registry'; export * from './state.module'; export * from './storage'; diff --git a/libs/cdk/state/src/link-registry/link-registry.selectors.ts b/libs/cdk/state/src/link-registry/link-registry.selectors.ts index 71d1fd87fc..ec66c14cb1 100644 --- a/libs/cdk/state/src/link-registry/link-registry.selectors.ts +++ b/libs/cdk/state/src/link-registry/link-registry.selectors.ts @@ -1,4 +1,3 @@ -import { UnionMember } from '@hra-ui/utils/types'; import { Selector } from '@ngxs/store'; import { LinkEntry, LinkId, LinkRegistryModel, LinkType } from './link-registry.model'; import { LinkRegistryQuery, LinkRegistryState } from './link-registry.state'; @@ -27,9 +26,9 @@ export class LinkRegistrySelectors { private static getEntry( state: LinkRegistryModel, id: LinkId, - type?: T - ): UnionMember | undefined { - const entry = state[id] as UnionMember; + type?: T, + ): Extract | undefined { + const entry = state[id] as Extract; const typeMatches = type === undefined || entry?.type === type; return typeMatches ? entry : undefined; } diff --git a/libs/cdk/state/src/link-registry/link-registry.state.ts b/libs/cdk/state/src/link-registry/link-registry.state.ts index 500deb1c0c..e0b3224cfa 100644 --- a/libs/cdk/state/src/link-registry/link-registry.state.ts +++ b/libs/cdk/state/src/link-registry/link-registry.state.ts @@ -1,8 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable, NgZone } from '@angular/core'; import { Router, UrlCreationOptions } from '@angular/router'; -import { createExternalUrl } from '@hra-ui/utils'; -import { UnionMember } from '@hra-ui/utils/types'; +import { createExternalUrl } from './url/create'; import { Action, State } from '@ngxs/store'; import { load } from 'js-yaml'; import { map, Observable } from 'rxjs'; @@ -10,19 +9,19 @@ import { Add, AddFromYaml, AddMany, LoadFromYaml, Navigate } from './link-regist import { ExternalLinkEntry, InternalLinkEntry, + LINK_REGISTRY_SCHEMA, LinkEntry, LinkId, LinkRegistryContext, LinkRegistryModel, LinkType, - LINK_REGISTRY_SCHEMA, } from './link-registry.model'; /** Query function for link entry optionally with type specified */ export type LinkRegistryQuery = ( id: LinkId, type?: T, -) => UnionMember | undefined; +) => Extract | undefined; /** State for keeping track of links globally */ @State({ diff --git a/libs/shared/utils/src/lib/url/create.spec.ts b/libs/cdk/state/src/link-registry/url/create.spec.ts similarity index 100% rename from libs/shared/utils/src/lib/url/create.spec.ts rename to libs/cdk/state/src/link-registry/url/create.spec.ts diff --git a/libs/shared/utils/src/lib/url/create.ts b/libs/cdk/state/src/link-registry/url/create.ts similarity index 96% rename from libs/shared/utils/src/lib/url/create.ts rename to libs/cdk/state/src/link-registry/url/create.ts index e510d68a9f..9a7b6d8794 100644 --- a/libs/shared/utils/src/lib/url/create.ts +++ b/libs/cdk/state/src/link-registry/url/create.ts @@ -2,7 +2,6 @@ import { LocationStrategy } from '@angular/common'; import { Injector, SecurityContext } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { ActivatedRoute, Params, Router, UrlCreationOptions } from '@angular/router'; -import { Any } from '@hra-ui/utils/types'; /** * Sets parameters with non-nullish values on a URLSearchParams @@ -27,7 +26,8 @@ function setQueryParams(dest: URLSearchParams, params: Params | null | undefined */ export function createInternalUrl( injector: Injector, - commands: Any[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + commands: any[], extras: UrlCreationOptions, isResourceUrl: boolean, ): string | undefined { diff --git a/libs/cdk/state/src/resource-registry/resource-registry.model.ts b/libs/cdk/state/src/resource-registry/resource-registry.model.ts index acd392ab65..a981c0b5f8 100644 --- a/libs/cdk/state/src/resource-registry/resource-registry.model.ts +++ b/libs/cdk/state/src/resource-registry/resource-registry.model.ts @@ -1,4 +1,3 @@ -import { UnionMember } from '@hra-ui/utils/types'; import { StateContext } from '@ngxs/store'; import * as z from 'zod'; @@ -41,7 +40,7 @@ export const RESOURCE_ID = z // ------------------------------------ /** Extracts the builtin entry with type T */ -type ExtractBuiltinEntryType = UnionMember, 'type', T>; +type ExtractBuiltinEntryType = Extract, { type: T }>; /** Maps raw builtin type strings to ResourceType */ type BuiltinTypes = { diff --git a/libs/common/analytics/src/lib/providers.ts b/libs/common/analytics/src/lib/providers.ts index 28fde3f23e..9c31554649 100644 --- a/libs/common/analytics/src/lib/providers.ts +++ b/libs/common/analytics/src/lib/providers.ts @@ -4,7 +4,7 @@ import { makeEnvironmentProviders, provideAppInitializer, } from '@angular/core'; -import { createFeature, getFeatureProviders, ProviderFeature } from '@hra-ui/common/util/providers'; +import { createProviderFeature, getProvidersForFeatures, ProviderFeature } from '@hra-ui/utils/di'; import { AnalyticsPlugin } from 'analytics'; import { AnalyticsErrorHandler, @@ -30,7 +30,7 @@ const enum AnalyticsFeatureKind { * @returns An analytics feature */ export function withErrorHandler(config: AnalyticsErrorHandlerConfig = {}): AnalyticsFeature { - return createFeature(AnalyticsFeatureKind.ErrorHandler, [ + return createProviderFeature(AnalyticsFeatureKind.ErrorHandler, [ provideAnalyticsErrorHandlerConfig(config), { provide: ErrorHandlerToken, @@ -46,7 +46,7 @@ export function withErrorHandler(config: AnalyticsErrorHandlerConfig = {}): Anal * @returns An analytics feature */ export function withPlugins(...plugins: (AnalyticsPlugin | (() => AnalyticsPlugin))[]): AnalyticsFeature { - return createFeature( + return createProviderFeature( AnalyticsFeatureKind.Plugins, plugins.map((plugin) => providePlugin(plugin)), ); @@ -58,7 +58,7 @@ export function withPlugins(...plugins: (AnalyticsPlugin | (() => AnalyticsPlugi * @returns An analytics feature */ export function withRouterEvents(): AnalyticsFeature { - return createFeature(AnalyticsFeatureKind.RouterEvents, [provideAppInitializer(setupRouterEventListener)]); + return createProviderFeature(AnalyticsFeatureKind.RouterEvents, [provideAppInitializer(setupRouterEventListener)]); } /** @@ -68,5 +68,5 @@ export function withRouterEvents(): AnalyticsFeature { * @returns Environment providers */ export function provideAnalytics(...features: AnalyticsFeature[]): EnvironmentProviders { - return makeEnvironmentProviders([...features.flatMap(getFeatureProviders)]); + return makeEnvironmentProviders(getProvidersForFeatures(features)); } diff --git a/libs/common/package.json b/libs/common/package.json index 5389369ff8..ee79eb9491 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -14,7 +14,8 @@ "type-fest": "^5.0.1", "@angular/forms": "^21.1.4", "nanoid": "^5.1.5", - "store2": "^2.14.4" + "store2": "^2.14.4", + "@hra-ui/utils": "0.0.1" }, "sideEffects": false } diff --git a/libs/common/router-ext/src/lib/fragment-link/fragment-link.directive.ts b/libs/common/router-ext/src/lib/fragment-link/fragment-link.directive.ts index d4725528ce..18fbac3002 100644 --- a/libs/common/router-ext/src/lib/fragment-link/fragment-link.directive.ts +++ b/libs/common/router-ext/src/lib/fragment-link/fragment-link.directive.ts @@ -1,5 +1,5 @@ import { Directive, input } from '@angular/core'; -import { stripLeadingHash } from '@hra-ui/common/url'; +import { stripLeadingHash } from '@hra-ui/utils/paths'; import { injectRouter } from '../injectors'; import { isAuxClick } from '../util/event'; diff --git a/libs/common/router-ext/src/lib/link-active/link-active.directive.ts b/libs/common/router-ext/src/lib/link-active/link-active.directive.ts index 4d9071c83b..41dd5256d5 100644 --- a/libs/common/router-ext/src/lib/link-active/link-active.directive.ts +++ b/libs/common/router-ext/src/lib/link-active/link-active.directive.ts @@ -1,8 +1,8 @@ import { computed, contentChildren, Directive, effect, inject, input, output } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { IsActiveMatchOptions, NavigationEnd } from '@angular/router'; +import { IsActiveMatchOptions, isActive as isUrlTreeActive, NavigationEnd } from '@angular/router'; import { injectWindow } from '@hra-ui/common/injectors'; -import { isUrlActive } from '@hra-ui/common/url'; +import { isUrlActive } from '@hra-ui/utils/paths'; import { createNotifier } from 'ngxtension/create-notifier'; import { injectRouter } from '../injectors'; import { LinkDirective } from '../link/link.directive'; @@ -98,7 +98,7 @@ export class LinkActiveDirective { return false; } - return router.isActive(urlTree, options); + return isUrlTreeActive(urlTree, router, options)(); }); } } diff --git a/libs/common/router-ext/src/lib/link/link.directive.ts b/libs/common/router-ext/src/lib/link/link.directive.ts index 75e2d90d81..192493b61f 100644 --- a/libs/common/router-ext/src/lib/link/link.directive.ts +++ b/libs/common/router-ext/src/lib/link/link.directive.ts @@ -1,6 +1,7 @@ import { LocationStrategy } from '@angular/common'; import { booleanAttribute, computed, Directive, inject, input } from '@angular/core'; -import { injectAppUrlResolver, isAbsolute, stripTrailingSlash } from '@hra-ui/common/url'; +import { injectAppUrlResolver } from '@hra-ui/common/url'; +import { isAbsoluteUrl, stripTrailingSlash } from '@hra-ui/utils/paths'; import { injectRouter } from '../injectors'; import { isAuxClick } from '../util/event'; @@ -30,7 +31,7 @@ export class LinkDirective { const { router, resolve } = this; if (router && !this.external()) { const url = resolve(this.url()); - if (!isAbsolute(url)) { + if (!isAbsoluteUrl(url)) { return router.parseUrl(stripTrailingSlash(url)); } } diff --git a/libs/common/router-ext/src/lib/providers.ts b/libs/common/router-ext/src/lib/providers.ts index 5d55aa4e3f..b7a1fe0c07 100644 --- a/libs/common/router-ext/src/lib/providers.ts +++ b/libs/common/router-ext/src/lib/providers.ts @@ -1,6 +1,6 @@ import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core'; import { Router } from '@angular/router'; -import { getFeatureProviders, ProviderFeature } from '@hra-ui/common/util/providers'; +import { getProvidersForFeatures, ProviderFeature } from '@hra-ui/utils/di'; import { provideRouter } from './injectors'; /** Router extension feature */ @@ -17,5 +17,5 @@ const enum RouterExtFeatureKind {} * @returns Environment providers */ export function provideRouterExt(...features: RouterExtFeature[]): EnvironmentProviders { - return makeEnvironmentProviders([provideRouter(() => inject(Router)), ...features.flatMap(getFeatureProviders)]); + return makeEnvironmentProviders([provideRouter(() => inject(Router)), ...getProvidersForFeatures(features)]); } diff --git a/libs/common/url/src/index.ts b/libs/common/url/src/index.ts index 33d387ecda..8216c6d10d 100644 --- a/libs/common/url/src/index.ts +++ b/libs/common/url/src/index.ts @@ -3,6 +3,4 @@ export { appUrl, AppUrlPipe, injectAppHref, injectAppUrlResolver, provideAppHref export { assetUrl, AssetUrlPipe, injectAssetHref, injectAssetUrlResolver, provideAssetHref } from './lib/url/asset'; export { cssUrl, CssUrlPipe } from './lib/url/css'; export { injectPageHref, injectPageUrlResolver, pageUrl, PageUrlPipe, providePageHref } from './lib/url/page'; -export { isUrlActive } from './lib/util/compare-url'; -export { isAbsolute, joinWithSlash, stripLeadingHash, stripTrailingSlash } from './lib/util/path'; export { InjectHrefFn, InjectUrlResolverFn, ProvideHrefFn, UrlResolverFn } from './lib/util/types'; diff --git a/libs/common/url/src/lib/url/asset.ts b/libs/common/url/src/lib/url/asset.ts index 8378c31a17..ebf47c57c9 100644 --- a/libs/common/url/src/lib/url/asset.ts +++ b/libs/common/url/src/lib/url/asset.ts @@ -1,8 +1,8 @@ import { Pipe, PipeTransform, signal, WritableSignal } from '@angular/core'; import { getImportMetaUrl } from '@hra-ui/common/import-meta'; +import { isAbsoluteUrl, joinWithSlashes } from '@hra-ui/utils/paths'; import { createInjectionToken } from 'ngxtension/create-injection-token'; import { createHrefProvider } from '../util/href-provider'; -import { isAbsolute, joinWithSlash } from '../util/path'; import { createUrlResolverFn, createUrlResolverInjector } from '../util/url-resolver'; /** @@ -29,7 +29,7 @@ function assetHref(): WritableSignal { * @returns The resolved url */ function resolveAssetUrl(href: string, value: string): string { - return isAbsolute(value) ? value : joinWithSlash(href, value); + return isAbsoluteUrl(value) ? value : joinWithSlashes(href, value); } /** Asset href */ diff --git a/libs/common/url/src/lib/url/page.ts b/libs/common/url/src/lib/url/page.ts index e9483b21a4..1ef9b7d6e5 100644 --- a/libs/common/url/src/lib/url/page.ts +++ b/libs/common/url/src/lib/url/page.ts @@ -1,8 +1,8 @@ import { LocationStrategy } from '@angular/common'; import { inject, Pipe, PipeTransform, signal, WritableSignal } from '@angular/core'; +import { isAbsoluteUrl, joinWithSlashes } from '@hra-ui/utils/paths'; import { createInjectionToken } from 'ngxtension/create-injection-token'; import { createHrefProvider } from '../util/href-provider'; -import { isAbsolute, joinWithSlash } from '../util/path'; import { createUrlResolverFn, createUrlResolverInjector } from '../util/url-resolver'; /** @@ -21,7 +21,7 @@ function pageHref(): WritableSignal { * @returns The resolved url */ function resolvePageUrl(href: string, value: string): string { - return isAbsolute(value) ? value : joinWithSlash(href, value); + return isAbsoluteUrl(value) ? value : joinWithSlashes(href, value); } /** Page href */ diff --git a/libs/common/url/src/lib/util/compare-url.spec.ts b/libs/common/url/src/lib/util/compare-url.spec.ts deleted file mode 100644 index 033b68e134..0000000000 --- a/libs/common/url/src/lib/util/compare-url.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { IsActiveMatchOptions } from '@angular/router'; -import { isUrlActive } from './compare-url'; - -describe('isUrlActive(targetUrl, currentUrl, options)', () => { - const targetEmpty = ''; - const targetSlashOnly = '/'; - const targetWithOrigin = 'https://example.com'; - const targetWithOrigin2 = 'https://other.example.com'; - const targetWithPath = '/foo/bar'; - const targetWithMatrixParams = '/foo;a=1;b=2/bar'; - const targetWithQuery = '/foo/bar?a=1'; - const targetWithFragment = '/foo/bar#abc'; - const targetWithEverything = '/foo;a=1;b=2/bar?a=1&b=2#abc'; - const currentEmpty = ''; - const currentWithOrigin = 'https://example.com'; - const currentWithPath = `${currentWithOrigin}/foo/bar`; - const currentWithPath2 = `${currentWithOrigin}/foo/bar/baz`; - const currentWithPath3 = `${currentWithOrigin}/abc/def`; - const currentWithMatrixParams = `${currentWithOrigin}/foo;a=1;b=2/bar`; - const currentWithMatrixParams2 = `${currentWithOrigin}/foo;b=2;a=1/bar`; - const currentWithMatrixParams3 = `${currentWithOrigin}/foo;a=1;b=2;c=3/bar`; - const currentWithMatrixParams4 = `${currentWithOrigin}/foo;a=3;b=2/bar`; - const currentWithQuery = `${currentWithOrigin}/foo/bar?a=1`; - const currentWithQuery2 = `${currentWithOrigin}/foo/bar?a=1&b=2`; - const currentWithQuery3 = `${currentWithOrigin}/foo/bar?b=2&a=1`; - const currentWithQuery4 = `${currentWithOrigin}/foo/bar?a=3`; - const currentWithFragment = `${currentWithOrigin}/foo/bar#abc`; - const currentWithFragment2 = `${currentWithOrigin}/foo/bar#def`; - const currentWithEverything = `${currentWithOrigin}/foo;a=1;b=2/bar?a=1&b=2#abc`; - - const defaultMatchOptions: IsActiveMatchOptions = { - fragment: 'ignored', - matrixParams: 'ignored', - paths: 'subset', - queryParams: 'ignored', - }; - - const testCases: [string, string, Partial, boolean][] = [ - // Origin checks - [targetEmpty, currentEmpty, {}, true], - [targetEmpty, currentWithOrigin, {}, true], - [targetSlashOnly, currentWithOrigin, {}, true], - [targetWithOrigin, currentWithOrigin, {}, true], - [targetWithOrigin, currentEmpty, {}, false], - [targetWithOrigin2, currentWithOrigin, {}, false], - - // Path checks - [targetWithPath, currentWithPath, {}, true], - [targetWithPath, currentWithPath2, {}, true], - [targetWithPath, currentWithPath, { paths: 'exact' }, true], - [targetWithPath, currentWithPath2, { paths: 'exact' }, false], - [targetWithPath, currentWithPath3, {}, false], - - // Matrix parameter checks - [targetWithMatrixParams, currentWithPath, {}, true], - [targetWithMatrixParams, currentWithMatrixParams, { matrixParams: 'exact' }, true], - [targetWithMatrixParams, currentWithMatrixParams2, { matrixParams: 'exact' }, true], - [targetWithMatrixParams, currentWithMatrixParams3, { matrixParams: 'subset' }, true], - [targetWithMatrixParams, currentWithMatrixParams3, { matrixParams: 'exact' }, false], - [targetWithMatrixParams, currentWithMatrixParams4, { matrixParams: 'subset' }, false], - [targetWithMatrixParams, currentWithMatrixParams4, { matrixParams: 'exact' }, false], - - // Query checks - [targetWithQuery, currentWithPath, {}, true], - [targetWithQuery, currentWithQuery, { queryParams: 'subset' }, true], - [targetWithQuery, currentWithQuery, { queryParams: 'exact' }, true], - [targetWithQuery, currentWithQuery2, { queryParams: 'subset' }, true], - [targetWithQuery, currentWithQuery3, { queryParams: 'subset' }, true], - [targetWithQuery, currentWithQuery2, { queryParams: 'exact' }, false], - [targetWithQuery, currentWithQuery4, { queryParams: 'subset' }, false], - [targetWithQuery, currentWithQuery4, { queryParams: 'exact' }, false], - - // Fragment checks - [targetWithFragment, currentWithPath, {}, true], - [targetWithFragment, currentWithFragment, { fragment: 'exact' }, true], - [targetWithFragment, currentWithPath, { fragment: 'exact' }, false], - [targetWithFragment, currentWithFragment2, { fragment: 'exact' }, false], - - // Everything check - [ - targetWithEverything, - currentWithEverything, - { paths: 'exact', matrixParams: 'exact', queryParams: 'exact', fragment: 'exact' }, - true, - ], - ]; - - it.each(testCases)( - "should match '%s' against '%s' with options %j with a result of %p", - (targetUrl, currentUrl, options, expected) => { - expect(isUrlActive(targetUrl, currentUrl, { ...defaultMatchOptions, ...options })).toEqual(expected); - }, - ); -}); diff --git a/libs/common/url/src/lib/util/compare-url.ts b/libs/common/url/src/lib/util/compare-url.ts deleted file mode 100644 index 74d6bcdb39..0000000000 --- a/libs/common/url/src/lib/util/compare-url.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { IsActiveMatchOptions } from '@angular/router'; -import { stripLeadingHash } from './path'; - -/** List of matrix parameter key/value pairs */ -type MatrixParams = [string, string][]; - -/** A path segment with matrix parameters */ -interface SegmentWithMatrixParams { - /** Segment name */ - segment: string; - /** Matrix parameters */ - matrix: MatrixParams; -} - -/** - * Test whether a set could possibly be a subset of another set purely based on their sizes. - * - * @param size1 Size of the first set - * @param size2 Size of the second set - * @param exact Whether the sizes should match exactly - * @returns true if the `size1` is less or equal to `size2`, false otherwise - */ -function checkSubsetSize(size1: number, size2: number, exact: boolean): boolean { - return exact ? size1 === size2 : size1 <= size2; -} - -/** - * Test whether a set is a subset of another set. - * Keys are compared case insensitively. - * - * @param iterable1 First set of key/value pairs - * @param iterable2 Second set of key/value pairs - * @returns true if the first set is a subset of the second one, false otherwise - */ -function checkSubset(iterable1: Iterable<[string, string]>, iterable2: Iterable<[string, string]>): boolean { - const entries1 = Array.from(iterable1); - const entries2 = Array.from(iterable2); - while (entries1.length > 0) { - const [key1, value1] = entries1.pop() as [string, string]; - const index = entries2.findIndex( - ([key2, value2]) => key1.toLowerCase() === key2.toLowerCase() && value1 === value2, - ); - if (index < 0) { - return false; - } - - entries2.splice(index, 1); - } - - return true; -} - -/** - * Parse a path segment that may contain matrix parameters. - * - * @param segment Raw segment string - * @returns A parsed segment object - */ -function parseSegment(segment: string): SegmentWithMatrixParams { - const parts = segment.split(';'); - const name = parts.shift() as string; - const matrix: MatrixParams = []; - - for (const param of parts) { - const [key, ...values] = param.split('='); - matrix.push([key, values.join('=')]); - } - - return { segment: name, matrix }; -} - -/** - * Parse a path that may contain matrix parameters in one or more segments. - * - * @param path Raw path - * @returns Array of path segments - */ -function parsePathSegments(path: string): SegmentWithMatrixParams[] { - path = path.replace(/^\/+|\/+$/g, ''); - if (!path) { - return []; - } - - return path.split('/').map(parseSegment); -} - -/** - * Check whether two sets of matrix parameters match. - * - * @param params1 First set of matrix parameters - * @param params2 Second set of matrix parameters - * @param options Comparison options - * @returns true if the first set matches the second set, false otherwise - */ -function compareMatrixParams(params1: MatrixParams, params2: MatrixParams, options: IsActiveMatchOptions): boolean { - if (options.matrixParams === 'ignored') { - return true; - } else if (!checkSubsetSize(params1.length, params2.length, options.matrixParams === 'exact')) { - return false; - } - - return checkSubset(params1, params2); -} - -/** - * Check whether two url paths match. - * - * @param path1 First raw path - * @param path2 Second raw path - * @param options Comparison options - * @returns true if the two paths matches, false otherwise - */ -function comparePathsWithMatrixParams(path1: string, path2: string, options: IsActiveMatchOptions): boolean { - if (path1 === path2) { - return true; - } - - const parsed1 = parsePathSegments(path1); - const parsed2 = parsePathSegments(path2); - if (!checkSubsetSize(parsed1.length, parsed2.length, options.paths === 'exact')) { - return false; - } - - for (let index = 0; index < parsed1.length; index++) { - const segment1 = parsed1[index]; - const segment2 = parsed2[index]; - if (segment1.segment !== segment2.segment || !compareMatrixParams(segment1.matrix, segment2.matrix, options)) { - return false; - } - } - - return true; -} - -/** - * Check whether two sets of query parameters match. - * - * @param params1 First set of query parameters - * @param params2 Second set of query parameters - * @param options Comparison options - * @returns true if the query parameters match, false otherwise - */ -function compareQueryParams( - params1: URLSearchParams, - params2: URLSearchParams, - options: IsActiveMatchOptions, -): boolean { - if (options.queryParams === 'ignored') { - return true; - } else if (!checkSubsetSize(params1.size, params2.size, options.queryParams === 'exact')) { - return false; - } - - // Typescript 5.9.3 is missing the iterator methods on URLSearchParams in lib.dom.d.ts - return checkSubset( - params1 as unknown as Iterable<[string, string]>, - params2 as unknown as Iterable<[string, string]>, - ); -} - -/** - * Check whether two url fragments match. - * - * @param fragment1 First fragment - * @param fragment2 Second fragment - * @param options Comparison options - * @returns true if the fragments match, false otherwise - */ -function compareFragments(fragment1: string, fragment2: string, options: IsActiveMatchOptions): boolean { - return options.fragment === 'ignored' || stripLeadingHash(fragment1) === stripLeadingHash(fragment2); -} - -/** - * Check whether a target url matches the current url based on the match options. - * - * @param targetUrl Url to test - * @param currentUrl Current/active url to test against - * @param options Comparison options - * @returns true if `targetUrl` matches `currentUrl`, false otherwise - */ -export function isUrlActive(targetUrl: string, currentUrl: string, options: IsActiveMatchOptions): boolean { - const base = 'http://localhost'; - const target = new URL(targetUrl, base); - const current = new URL(currentUrl, base); - const sameOrigin = target.origin === current.origin || target.origin === base; - - return ( - sameOrigin && - comparePathsWithMatrixParams(target.pathname, current.pathname, options) && - compareQueryParams(target.searchParams, current.searchParams, options) && - compareFragments(target.hash, current.hash, options) - ); -} diff --git a/libs/common/url/src/lib/util/path.spec.ts b/libs/common/url/src/lib/util/path.spec.ts deleted file mode 100644 index bccddaf8e9..0000000000 --- a/libs/common/url/src/lib/util/path.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { isAbsolute, joinWithSlash, stripLeadingHash, stripTrailingSlash } from './path'; - -describe('isAbsolute(path)', () => { - it('returns true for abolute urls', () => { - expect(isAbsolute('https://example.com')).toBeTruthy(); - expect(isAbsolute('file://a.csv')).toBeTruthy(); - }); - - it('returns false for relative urls', () => { - expect(isAbsolute('test.json')).toBeFalsy(); - expect(isAbsolute('./assets/image.png')).toBeFalsy(); - expect(isAbsolute('/main.js')).toBeFalsy(); - }); -}); - -describe('joinWithSlash(start, end)', () => { - it('should join to paths', () => { - expect(joinWithSlash('', '')).toEqual(''); - expect(joinWithSlash('', 'a')).toEqual('a'); - expect(joinWithSlash('b', '')).toEqual('b'); - expect(joinWithSlash('c', 'd')).toEqual('c/d'); - expect(joinWithSlash('e/', 'f')).toEqual('e/f'); - expect(joinWithSlash('g', '/h')).toEqual('g/h'); - expect(joinWithSlash('i/', '/j')).toEqual('i/j'); - }); -}); - -describe('stripTrailingSlash(path)', () => { - it('should remove the trailing slash while preserving the fragment and query parameters', () => { - expect(stripTrailingSlash('a')).toEqual('a'); - expect(stripTrailingSlash('b/')).toEqual('b'); - expect(stripTrailingSlash('c/#frag')).toEqual('c#frag'); - expect(stripTrailingSlash('d/?param=value')).toEqual('d?param=value'); - expect(stripTrailingSlash('e/#frag/?param=val')).toEqual('e#frag/?param=val'); - }); -}); - -describe('stripLeadingHash(fragment)', () => { - it('should remove the leading hash if present', () => { - expect(stripLeadingHash('')).toEqual(''); - expect(stripLeadingHash('a')).toEqual('a'); - expect(stripLeadingHash('#b')).toEqual('b'); - }); -}); diff --git a/libs/common/url/src/lib/util/path.ts b/libs/common/url/src/lib/util/path.ts deleted file mode 100644 index b49a9199f9..0000000000 --- a/libs/common/url/src/lib/util/path.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Test whether a path is an absolute url - * - * @param path Path to test - * @returns true if the path is absolute, false otherwise - */ -export function isAbsolute(path: string): boolean { - try { - new URL(path); - return true; - } catch { - return false; - } -} - -/** - * Joins two paths with a signle slash between them - * - * @param start First path - * @param end Second path - * @returns The concatenated path - */ -export function joinWithSlash(start: string, end: string): string { - if (!start) { - return end; - } else if (!end) { - return start; - } - - start = start.replace(/\/+$/, ''); - end = end.replace(/^\/+/, ''); - return `${start}/${end}`; -} - -/** - * Remove the trailing slash from a path while preserving the fragment and query parameters (if present) - * - * @param path Url to strip - * @returns New url - */ -export function stripTrailingSlash(path: string): string { - const index = path.search(/#|\?|$/); - if (path[index - 1] === '/') { - return path.slice(0, index - 1) + path.slice(index); - } - - return path; -} - -/** - * Remove the leading hash symbol from a fragment (if present) - * - * @param fragment Fragment to strip - * @returns New fragment - */ -export function stripLeadingHash(fragment: string): string { - if (fragment && fragment[0] === '#') { - return fragment.slice(1); - } - - return fragment; -} diff --git a/libs/common/util/providers/src/index.ts b/libs/common/util/providers/src/index.ts deleted file mode 100644 index 9ed49e93d6..0000000000 --- a/libs/common/util/providers/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createFeature, getFeatureProviders, ProviderFeature } from './lib/feature'; diff --git a/libs/common/util/providers/src/lib/feature.spec.ts b/libs/common/util/providers/src/lib/feature.spec.ts deleted file mode 100644 index ea054907e1..0000000000 --- a/libs/common/util/providers/src/lib/feature.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { makeEnvironmentProviders } from '@angular/core'; -import { createFeature, getFeatureProviders } from './feature'; - -describe('ProviderFeature utility', () => { - it('should create a feature with providers', () => { - const providers = [makeEnvironmentProviders([])]; - const feature = createFeature('abc', providers); - expect(getFeatureProviders(feature)).toBe(providers); - }); -}); diff --git a/libs/common/util/providers/src/lib/feature.ts b/libs/common/util/providers/src/lib/feature.ts deleted file mode 100644 index b7ae89a27b..0000000000 --- a/libs/common/util/providers/src/lib/feature.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { EnvironmentProviders, Provider } from '@angular/core'; - -/** Either a regular provider or environment providers */ -export type AnyProvider = Provider | EnvironmentProviders; - -/** A provider feature */ -export interface ProviderFeature { - /** Feature kind. Usually an integer enum value but can be anything. */ - [FEATURE_KIND]: F; - /** Array of providers */ - [FEATURE_PROVIDERS]: P[]; -} - -/** Key used to store the feature kind */ -const FEATURE_KIND = Symbol(); -/** Key used to store the feature providers */ -const FEATURE_PROVIDERS = Symbol(); - -/** - * Create a new feature object with a kind and providers - * - * @param kind Feature kind - * @param providers Feature providers - * @returns A feature object - */ -export function createFeature(kind: F, providers: P[]): ProviderFeature { - return { [FEATURE_KIND]: kind, [FEATURE_PROVIDERS]: providers }; -} - -/** - * Get the providers from a feature object - * - * @param feature Feature object - * @returns Array of providers - */ -export function getFeatureProviders(feature: ProviderFeature): P[] { - return feature[FEATURE_PROVIDERS]; -} diff --git a/libs/design-system/icons/src/lib/resolvers/icon.resolver.ts b/libs/design-system/icons/src/lib/resolvers/icon.resolver.ts index f0b49926fd..753a9feb33 100644 --- a/libs/design-system/icons/src/lib/resolvers/icon.resolver.ts +++ b/libs/design-system/icons/src/lib/resolvers/icon.resolver.ts @@ -1,7 +1,8 @@ import { assertInInjectionContext, inject } from '@angular/core'; import { IconResolver } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; -import { assetUrl, joinWithSlash } from '@hra-ui/common/url'; +import { assetUrl } from '@hra-ui/common/url'; +import { joinWithSlashes } from '@hra-ui/utils/paths'; import { SVG_ICON_DIRECTORY } from '../utils/tokens'; /** @@ -18,9 +19,7 @@ export function createSvgIconResolver(): IconResolver { const sanitizer = inject(DomSanitizer); const baseUrl = assetUrl(directory); return (name, namespace) => { - let path = baseUrl(); - path = joinWithSlash(path, namespace); - path = joinWithSlash(path, `${name}.svg`); + const path = joinWithSlashes(baseUrl(), namespace, `${name}.svg`); return sanitizer.bypassSecurityTrustResourceUrl(path); }; } diff --git a/libs/design-system/navigation/site-navigation/src/lib/site-navigation.component.ts b/libs/design-system/navigation/site-navigation/src/lib/site-navigation.component.ts index 256f0859db..fdcfd774f7 100644 --- a/libs/design-system/navigation/site-navigation/src/lib/site-navigation.component.ts +++ b/libs/design-system/navigation/site-navigation/src/lib/site-navigation.component.ts @@ -8,9 +8,10 @@ import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { RouterModule } from '@angular/router'; import { HraCommonModule } from '@hra-ui/common'; -import { injectAppUrlResolver, isAbsolute } from '@hra-ui/common/url'; import { injectRouter } from '@hra-ui/common/router-ext'; +import { injectAppUrlResolver } from '@hra-ui/common/url'; import { ButtonsModule } from '@hra-ui/design-system/buttons'; +import { isAbsoluteUrl } from '@hra-ui/utils/paths'; import { NgScrollbar } from 'ngx-scrollbar'; import { injectNavigationEnd } from 'ngxtension/navigation-end'; import { NavigationCategoryComponent } from './navigation-category/navigation-category.component'; @@ -84,7 +85,7 @@ export class SiteNavigationComponent { for (const category of categories) { for (const item of category.children) { const resolvedUrl = this.urlResolver(item.url); - if (!isAbsolute(resolvedUrl) && this.router?.isActive(resolvedUrl, ACTIVE_MATCH_OPTIONS)) { + if (!isAbsoluteUrl(resolvedUrl) && this.router?.isActive(resolvedUrl, ACTIVE_MATCH_OPTIONS)) { return category.label; } } diff --git a/libs/design-system/package.json b/libs/design-system/package.json index 47a9fcb46c..dd4b686bb0 100644 --- a/libs/design-system/package.json +++ b/libs/design-system/package.json @@ -50,7 +50,8 @@ "@angular/forms": "^21.1.4", "@google/model-viewer": "4.1.0", "store2": "^2.14.4", - "type-fest": "^5.0.1" + "type-fest": "^5.0.1", + "@hra-ui/utils": "0.0.1" }, "sideEffects": false } diff --git a/libs/design-system/scrolling/src/lib/scrolling.module.ts b/libs/design-system/scrolling/src/lib/scrolling.module.ts index 88ea177da3..a4ac5623cf 100644 --- a/libs/design-system/scrolling/src/lib/scrolling.module.ts +++ b/libs/design-system/scrolling/src/lib/scrolling.module.ts @@ -2,14 +2,13 @@ import { CdkScrollable } from '@angular/cdk/scrolling'; import { EnvironmentProviders, makeEnvironmentProviders, NgModule } from '@angular/core'; import { provideStyleComponents } from '@hra-ui/cdk/styling'; import { getImportMetaUrl } from '@hra-ui/common/import-meta'; -import { joinWithSlash } from '@hra-ui/common/url'; +import { joinWithSlashes } from '@hra-ui/utils/paths'; import { NgScrollbarModule, NgScrollbarOptions, provideScrollbarOptions, provideScrollbarPolyfill, } from 'ngx-scrollbar'; - import { ScrollOverflowFadeDirective } from './scroll-overflow-fade/scroll-overflow-fade.directive'; import { ScrollbarStylesComponent } from './scrollbar-styles/scrollbar-styles.component'; @@ -32,7 +31,7 @@ export function provideScrolling(options?: ScrollingOptions): EnvironmentProvide const metaUrl = getImportMetaUrl(); const href = /^https?:/.test(metaUrl) ? new URL('./', metaUrl).toString() : ''; // TODO: Find a better way to resolve the polyfill url - const polyfillUrl = joinWithSlash(href, options?.polyfillUrl ?? DEFAULT_POLYFILL_URL); + const polyfillUrl = joinWithSlashes(href, options?.polyfillUrl ?? DEFAULT_POLYFILL_URL); return makeEnvironmentProviders([ provideStyleComponents(ScrollbarStylesComponent), diff --git a/libs/shared/utils/README.md b/libs/hra-ui/analytics/README.md similarity index 53% rename from libs/shared/utils/README.md rename to libs/hra-ui/analytics/README.md index 146a8b829e..84badeeb6d 100644 --- a/libs/shared/utils/README.md +++ b/libs/hra-ui/analytics/README.md @@ -1,7 +1,7 @@ -# shared-utils +# analytics This library was generated with [Nx](https://nx.dev). ## Running unit tests -Run `nx test shared-utils` to execute the unit tests. +Run `nx test analytics` to execute the unit tests. diff --git a/libs/shared/utils/eslint.config.mjs b/libs/hra-ui/analytics/eslint.config.mjs similarity index 100% rename from libs/shared/utils/eslint.config.mjs rename to libs/hra-ui/analytics/eslint.config.mjs diff --git a/libs/common/util/providers/ng-package.json b/libs/hra-ui/analytics/events/ng-package.json similarity index 100% rename from libs/common/util/providers/ng-package.json rename to libs/hra-ui/analytics/events/ng-package.json diff --git a/libs/hra-ui/analytics/events/src/index.ts b/libs/hra-ui/analytics/events/src/index.ts new file mode 100644 index 0000000000..a7941dd3a2 --- /dev/null +++ b/libs/hra-ui/analytics/events/src/index.ts @@ -0,0 +1,10 @@ +export { CoreEvents } from './lib/core-events'; +export { AnalyticsEvent, createEvent, EventCategory, EventType } from './lib/event'; +export { ClickEventProps } from './lib/events/click'; +export { ErrorEventProps } from './lib/events/error'; +export { HoverEventProps } from './lib/events/hover'; +export { KeyboardEventProps } from './lib/events/keyboard'; +export { ModelChangeEventProps } from './lib/events/model-change'; +export { PageViewEventProps } from './lib/events/page-view'; +export { CommonEventProps, EventPayload, EventPayloadFor, EventProps, EventPropsFor } from './lib/payload'; +export { EventTrigger, EventTriggerModifier, EventTriggerPayload, EventTriggerPayloadFor } from './lib/trigger'; diff --git a/libs/hra-ui/analytics/events/src/lib/core-events.ts b/libs/hra-ui/analytics/events/src/lib/core-events.ts new file mode 100644 index 0000000000..8250a55303 --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/core-events.ts @@ -0,0 +1,21 @@ +import Click from './events/click'; +import DoubleClick from './events/double-click'; +import Error from './events/error'; +import Hover from './events/hover'; +import Keyboard from './events/keyboard'; +import ModelChange from './events/model-change'; +import PageView from './events/page-view'; + +/** Core events type map */ +export type CoreEvents = typeof CoreEvents; + +/** Core events for use in all applications */ +export const CoreEvents = { + Click, + DoubleClick, + Error, + Hover, + Keyboard, + ModelChange, + PageView, +} as const; diff --git a/libs/hra-ui/analytics/events/src/lib/event.spec.ts b/libs/hra-ui/analytics/events/src/lib/event.spec.ts new file mode 100644 index 0000000000..8e2e4dd39e --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/event.spec.ts @@ -0,0 +1,15 @@ +import { createEvent, EventCategory } from './event'; + +describe('Analytics Event System', () => { + it('should create an event with type, category, and optionally trigger', () => { + const type = 'test'; + const trigger = 'click'; + + const event = createEvent(type, EventCategory.Necessary); + expect(event.type).toBe(type); + expect(event.category).toBe(EventCategory.Necessary); + + const event2 = createEvent(type, EventCategory.Necessary, trigger); + expect(event2.trigger).toBe(trigger); + }); +}); diff --git a/libs/hra-ui/analytics/events/src/lib/event.ts b/libs/hra-ui/analytics/events/src/lib/event.ts new file mode 100644 index 0000000000..b3185ca0df --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/event.ts @@ -0,0 +1,37 @@ +import { Tagged } from 'type-fest'; +import { EventPayloadContainer } from './payload'; +import { EventTrigger } from './trigger'; + +/** Event type */ +export type EventType = Tagged; + +/** Event categories used to filter events based on privacy settings */ +export enum EventCategory { + Necessary = 'necessary', + Statistics = 'statistics', + Preferences = 'preferences', + Marketing = 'marketing', +} + +/** An event specification */ +export interface AnalyticsEvent

extends EventPayloadContainer

{ + /** Event type */ + type: EventType; + /** Event category */ + category: EventCategory; + /** Event default trigger */ + trigger?: EventTrigger; +} + +/** + * Create a new event specification. + * + * @template P Event payload type + * @param type Event type + * @param category Event category + * @param trigger Default event trigger + * @returns A new event specification + */ +export function createEvent

(type: string, category: EventCategory, trigger?: EventTrigger): AnalyticsEvent

{ + return { type: type as EventType, category, trigger }; +} diff --git a/libs/hra-ui/analytics/events/src/lib/events/click.ts b/libs/hra-ui/analytics/events/src/lib/events/click.ts new file mode 100644 index 0000000000..48a32fca18 --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/events/click.ts @@ -0,0 +1,7 @@ +import { createEvent, EventCategory } from '../event'; + +/** Click event properties */ +export type ClickEventProps = object; + +/** Click event */ +export default createEvent('click', EventCategory.Statistics, 'click'); diff --git a/libs/hra-ui/analytics/events/src/lib/events/double-click.ts b/libs/hra-ui/analytics/events/src/lib/events/double-click.ts new file mode 100644 index 0000000000..dbabbf718e --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/events/double-click.ts @@ -0,0 +1,7 @@ +import { createEvent, EventCategory } from '../event'; + +/** Double click event properties */ +export type DoubleClickEventProps = object; + +/** Double click event */ +export default createEvent('doubleClick', EventCategory.Statistics, 'dblclick'); diff --git a/libs/hra-ui/analytics/events/src/lib/events/error.ts b/libs/hra-ui/analytics/events/src/lib/events/error.ts new file mode 100644 index 0000000000..0873dc64b9 --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/events/error.ts @@ -0,0 +1,14 @@ +import { createEvent, EventCategory } from '../event'; + +/** Error event properties */ +export interface ErrorEventProps { + /** An error message */ + message: string; + /** Contextual data */ + context?: unknown; + /** Reason for the error (usually an `Error` object) */ + reason?: unknown; +} + +/** Error event */ +export default createEvent('error', EventCategory.Necessary); diff --git a/libs/hra-ui/analytics/events/src/lib/events/hover.ts b/libs/hra-ui/analytics/events/src/lib/events/hover.ts new file mode 100644 index 0000000000..68228511e1 --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/events/hover.ts @@ -0,0 +1,7 @@ +import { createEvent, EventCategory } from '../event'; + +/** Hover event properties */ +export type HoverEventProps = object; + +/** Hover event */ +export default createEvent('hover', EventCategory.Statistics, 'mouseenter'); diff --git a/libs/hra-ui/analytics/events/src/lib/events/keyboard.ts b/libs/hra-ui/analytics/events/src/lib/events/keyboard.ts new file mode 100644 index 0000000000..3ca4912740 --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/events/keyboard.ts @@ -0,0 +1,7 @@ +import { createEvent, EventCategory } from '../event'; + +/** Keyboard event properties */ +export type KeyboardEventProps = object; + +/** Keyboard event */ +export default createEvent('keyboard', EventCategory.Statistics, 'keydown'); diff --git a/libs/hra-ui/analytics/events/src/lib/events/model-change.ts b/libs/hra-ui/analytics/events/src/lib/events/model-change.ts new file mode 100644 index 0000000000..f71254d74b --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/events/model-change.ts @@ -0,0 +1,10 @@ +import { createEvent, EventCategory } from '../event'; + +/** Model change event properties */ +export interface ModelChangeEventProps { + /** Model value */ + value?: unknown; +} + +/** Model change event */ +export default createEvent('modelChange', EventCategory.Statistics); diff --git a/libs/hra-ui/analytics/events/src/lib/events/page-view.ts b/libs/hra-ui/analytics/events/src/lib/events/page-view.ts new file mode 100644 index 0000000000..db2ea3f88c --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/events/page-view.ts @@ -0,0 +1,27 @@ +import { createEvent, EventCategory } from '../event'; + +/** + * Page view properties + * Provided by [`analytics`](https://github.com/DavidWells/analytics/blob/analytics%400.8.19/packages/analytics-core/src/modules/page.js#L54) + */ +export interface PageViewEventProps { + /** Page title */ + title: string; + /** Page origin */ + url: string; + /** Current path */ + path: string; + /** Current hash */ + hash: string; + /** Current search (query parameters) */ + search: string; + /** Viewport width at time of event */ + width: number; + /** Viewport height at time of event */ + height: number; + /** Referrer page */ + referrer?: string; +} + +/** Page view event */ +export default createEvent('pageView', EventCategory.Statistics); diff --git a/libs/hra-ui/analytics/events/src/lib/payload.ts b/libs/hra-ui/analytics/events/src/lib/payload.ts new file mode 100644 index 0000000000..35589292ed --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/payload.ts @@ -0,0 +1,48 @@ +import { HasRequiredKeys, Simplify } from 'type-fest'; + +/** + * Unique symbol used to store payload type information. + * No need to initialize as it is only ever used in type contexts. + */ +declare const PAYLOAD: unique symbol; + +/** Whether the payload has no required properties */ +type SupportsEmptyPayload

= P extends object ? (HasRequiredKeys

extends true ? false : true) : false; + +/** Common properties that may be available on any analytics event */ +export interface CommonEventProps { + /** Full feature path to the triggering component */ + path?: string; + /** The underlying (usually DOM) event that triggered analytics */ + trigger?: string; + /** Additional data related to the triggering event */ + triggerData?: Event; +} + +/** + * A container type with payload type information. + * Used to bind the payload type to an event type without + * requiring an actual payload property on event objects. + */ +export interface EventPayloadContainer

{ + /** + * Payload type. Not actually present on event objects. + * @private + */ + [PAYLOAD]?: EventPayload

; +} + +/** Event payload type with common and event specific properties */ +export type EventPayload

= Simplify>; + +/** Extract the payload type for an event */ +export type EventPayloadFor = T extends EventPayloadContainer ? EventPayload

: never; + +/** + * Event properties type, which is either the full payload or an empty string if the payload has no required properties. + * Used by event directives to allow omitting the payload argument when all payload properties are optional. + */ +export type EventProps

= EventPayload

| (SupportsEmptyPayload

extends true ? '' : never); + +/** Extract the properties type for an event */ +export type EventPropsFor = T extends EventPayloadContainer ? EventProps

: never; diff --git a/libs/hra-ui/analytics/events/src/lib/trigger.ts b/libs/hra-ui/analytics/events/src/lib/trigger.ts new file mode 100644 index 0000000000..15c3f7a581 --- /dev/null +++ b/libs/hra-ui/analytics/events/src/lib/trigger.ts @@ -0,0 +1,14 @@ +/** Union of all builtin dom event names */ +export type DOMEventName = keyof GlobalEventHandlersEventMap; + +/** Modifiers that can be prepended to event names to change their scope */ +export type EventTriggerModifier = 'window:' | 'document:' | 'body:'; + +/** A source DOM event that's triggering a corresponding analytics event */ +export type EventTrigger = E | `${EventTriggerModifier}${E}`; + +/** The payload for a given DOM event */ +export type EventTriggerPayload = GlobalEventHandlersEventMap[T]; + +/** Extract the payload type for a given event trigger */ +export type EventTriggerPayloadFor = T extends EventTrigger ? EventTriggerPayload : never; diff --git a/libs/hra-ui/analytics/jest.config.ts b/libs/hra-ui/analytics/jest.config.ts new file mode 100644 index 0000000000..56c5ea7684 --- /dev/null +++ b/libs/hra-ui/analytics/jest.config.ts @@ -0,0 +1,6 @@ +export default { + displayName: 'analytics', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/hra-ui/analytics', +}; diff --git a/libs/hra-ui/analytics/ng-package.json b/libs/hra-ui/analytics/ng-package.json new file mode 100644 index 0000000000..a159a1f8f1 --- /dev/null +++ b/libs/hra-ui/analytics/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/hra-ui/analytics", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/hra-ui/analytics/package.json b/libs/hra-ui/analytics/package.json new file mode 100644 index 0000000000..3520064492 --- /dev/null +++ b/libs/hra-ui/analytics/package.json @@ -0,0 +1,6 @@ +{ + "name": "@hra-ui/analytics", + "version": "0.0.1", + "peerDependencies": {}, + "sideEffects": false +} diff --git a/libs/shared/utils/types/ng-package.json b/libs/hra-ui/analytics/plugins/hra-analytics/ng-package.json similarity index 100% rename from libs/shared/utils/types/ng-package.json rename to libs/hra-ui/analytics/plugins/hra-analytics/ng-package.json diff --git a/libs/hra-ui/analytics/plugins/hra-analytics/src/index.ts b/libs/hra-ui/analytics/plugins/hra-analytics/src/index.ts new file mode 100644 index 0000000000..4b44c6dccc --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-analytics/src/index.ts @@ -0,0 +1,2 @@ +export { hraAnalyticsPlugin, HraAnalyticsPluginConfig } from './lib/plugin'; +export { provideTelemetryOptions, TelemetryOptions, TelemetryService } from './lib/telemetry/telemetry.service'; diff --git a/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/plugin.spec.ts b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/plugin.spec.ts new file mode 100644 index 0000000000..a9b82224a1 --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/plugin.spec.ts @@ -0,0 +1,53 @@ +import { TestBed } from '@angular/core/testing'; +import { hraAnalyticsPlugin } from './plugin'; +import { TelemetryService } from './telemetry/telemetry.service'; +import { Analytics } from 'analytics'; +import { CoreEvents } from '@hra-ui/common/analytics/events'; + +describe('hra-analytics plugin', () => { + const options = { sessionId: 'test' }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { + provide: TelemetryService, + useValue: { send: jest.fn() }, + }, + ], + }); + }); + + it('must be called in an injection context', () => { + expect(() => hraAnalyticsPlugin(options)).toThrow(); + }); + + it('sends page and track events to the telemetry service', async () => { + const plugin = TestBed.runInInjectionContext(() => hraAnalyticsPlugin(options)); + const telemetry = TestBed.inject(TelemetryService); + const config = { sv: 0, app: 'myapp', version: '1.2', path: undefined, trigger: undefined, triggerData: undefined }; + const path = 'foo.bar'; + const payload = { custom: 'data' }; + const analytics = Analytics({ + ...config, + plugins: [plugin], + }); + + await analytics.page(payload); + expect(telemetry.send).toHaveBeenCalledWith({ + ...config, + sessionId: options.sessionId, + event: CoreEvents.PageView.type, + e: expect.objectContaining(payload), + }); + + await analytics.track(CoreEvents.Click.type, { path, ...payload }); + expect(telemetry.send).toHaveBeenCalledWith({ + ...config, + sessionId: options.sessionId, + event: CoreEvents.Click.type, + path: path, + e: expect.objectContaining(payload), + }); + }); +}); diff --git a/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/plugin.ts b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/plugin.ts new file mode 100644 index 0000000000..6ad461bbba --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/plugin.ts @@ -0,0 +1,93 @@ +import { inject, Injector } from '@angular/core'; +import { CoreEvents, EventPayload } from '@hra-ui/analytics/events'; +import { AnalyticsInstance, AnalyticsPlugin, PageData } from 'analytics'; +import { assertInjector } from 'ngxtension/assert-injector'; +import { TelemetryService } from './telemetry/telemetry.service'; + +/** Plugin configuration */ +export interface HraAnalyticsPluginConfig { + /** Session identifier */ + sessionId: string; + /** Plugin injector context */ + injector?: Injector; +} + +/** + * Interface for data passed by the 'analytics' library to plugin callbacks. + * Only declares the fields that are used by the hra-analytics plugin. + */ +interface EventData { + /** Plugin configuration */ + config: HraAnalyticsPluginConfig; + /** Reference to the owning analytics instance */ + instance: AnalyticsInstance; + /** Payload data */ + payload: { + /** Event type */ + event: string; + /** User data */ + properties: PageData | EventPayload; + }; +} + +/** + * An `analytics` plugin that logs events to a hra endpoint + * + * @param pluginConfig Plugin configuration + * @returns An analytics plugin + */ +export function hraAnalyticsPlugin(pluginConfig: HraAnalyticsPluginConfig): AnalyticsPlugin { + return assertInjector(hraAnalyticsPlugin, pluginConfig.injector, () => { + const telemetry = inject(TelemetryService); + + return { + name: 'hra-analytics', + config: pluginConfig, + page({ config, instance, payload }: EventData) { + telemetry.send(buildEventData(instance, config, CoreEvents.PageView.type, payload.properties)); + }, + track({ config, instance, payload: { event, properties } }: EventData) { + telemetry.send(buildEventData(instance, config, event, properties)); + }, + }; + }); +} + +/** + * Builds the data to send to analytics. + * Adds common fields like app, version, etc., and normalizes event payloads. + * + * @param instance Analytics instance + * @param config Plugin config + * @param event Event name + * @param props Event properties + * @returns Data to send to analytics + */ +function buildEventData( + instance: AnalyticsInstance, + config: HraAnalyticsPluginConfig, + event: string, + props: PageData | EventPayload, +): object { + const { trigger, triggerData } = props; + let path: string | undefined = undefined; + if (event !== CoreEvents.PageView.type) { + path = props.path; + props = { ...props }; + delete props.path; + delete props.trigger; + delete props.triggerData; + } + + return { + sv: 0, + sessionId: config.sessionId, + app: instance.getState('context.app'), + version: instance.getState('context.version'), + event, + path, + trigger, + triggerData, + e: props, + }; +} diff --git a/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/telemetry/telemetry.service.spec.ts b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/telemetry/telemetry.service.spec.ts new file mode 100644 index 0000000000..0372a236e6 --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/telemetry/telemetry.service.spec.ts @@ -0,0 +1,70 @@ +import { TestBed } from '@angular/core/testing'; +import { provideFetchImpl, provideTelemetryOptions, TelemetryService } from './telemetry.service'; +import { waitFor } from '@testing-library/angular'; + +describe('TelemetryService', () => { + class Range { + constructor( + readonly from: number, + readonly to: number, + ) {} + } + + function serializeRange(_prefix: string, value: unknown): unknown { + return value instanceof Range ? `${value.from}..${value.to}` : value; + } + + function filterPrefix(prefix: string, value: unknown): unknown { + return prefix.startsWith('skip') ? undefined : value; + } + + function setup() { + const fetch = jest.fn().mockResolvedValue({}); + + TestBed.configureTestingModule({ + providers: [ + provideTelemetryOptions({ + filters: [filterPrefix, serializeRange], + }), + provideFetchImpl({ fetch }), + ], + }); + + const service = TestBed.inject(TelemetryService); + return { service, fetch }; + } + + const data = { + value: 'abc', + options: { range: new Range(10, 30) }, + date: new Date(0), + skipThis: 'skipped', + skip: { that: 123 }, + }; + const dataQueryString = 'value=abc&options.range=10..30&date=1970-01-01T00%3A00%3A00.000Z'; + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should send the data to the endpoint', () => { + const { service, fetch } = setup(); + + service.send(data); + expect(fetch).toHaveBeenCalledWith(expect.stringContaining(dataQueryString), { + method: 'GET', + cache: 'no-store', + keepalive: true, + }); + }); + + it('should catch fetch errors and log them to console', async () => { + const { service, fetch } = setup(); + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const error = new Error('Network error'); + fetch.mockRejectedValueOnce(error); + + expect(() => service.send(data)).not.toThrow(); + await waitFor(() => expect(consoleErrorSpy).toHaveBeenCalledWith(expect.any(String), error)); + }); +}); diff --git a/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/telemetry/telemetry.service.ts b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/telemetry/telemetry.service.ts new file mode 100644 index 0000000000..c9d254ea1d --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/telemetry/telemetry.service.ts @@ -0,0 +1,129 @@ +import { Injectable, isDevMode } from '@angular/core'; +import { createInjectionToken } from 'ngxtension/create-injection-token'; +import { stringify } from 'qs'; +import { safeTruncateQueryString } from '../util/safe-truncate'; +import { serialize } from '../util/serialize'; + +/** + * A telemetry parameter filter that is applied to each query parameter during serialization. + * Can be used to both remove query parameters or to apply custom serialization logic to specific parameters. + */ +export type TelemetryParameterFilter = (prefix: string, value: unknown) => unknown; + +/** + * Options for the TelemetryService, which can be provided via dependency injection. + * + * @see {@link provideTelemetryOptions} + */ +export interface TelemetryOptions { + /** The endpoint to which telemetry data is sent */ + endpoint?: string; + /** Query parameter filters to apply */ + filters?: TelemetryParameterFilter[]; + /** Maximum length of each query string sent to the endpoint */ + maxQueryLength?: number; + /** Suffix appended to query parameters that have been truncated in order to fit the maximum query length */ + truncatedSuffix?: string; +} + +/** DI token for telemetry options */ +const OPTIONS_TOKEN = createInjectionToken((): TelemetryOptions => ({})); + +/** Injects telemetry options provided via dependency injection. */ +export const injectTelemetryOptions = OPTIONS_TOKEN[0]; +/** + * Provides telemetry options for the TelemetryService via dependency injection. + * Can be used to override default options such as the endpoint URL, or to provide custom filters for telemetry data. + * + * @see {@link TelemetryOptions} + */ +export const provideTelemetryOptions = OPTIONS_TOKEN[1]; + +/** + * DI token for fetch implementation. + * Used to override the default fetch implementation during testing + */ +const FETCH_IMPL_TOKEN = createInjectionToken(() => ({ fetch })); + +/** Injects the fetch implementation */ +export const injectFetchImpl = FETCH_IMPL_TOKEN[0]; +/** Provide a different fetch implementation */ +export const provideFetchImpl = FETCH_IMPL_TOKEN[1]; + +/** + * Telemetry service responsible for sending analytics data to the HRA analytics endpoint + */ +@Injectable({ + providedIn: 'root', +}) +export class TelemetryService { + /** Telemetry options */ + readonly options = { + endpoint: `https://cdn.humanatlas.io/tr${isDevMode() ? '-dev' : ''}`, + filters: [], + maxQueryLength: 7000, + ...injectTelemetryOptions(), + }; + + /** Fetch implementation */ + private readonly fetchImpl = injectFetchImpl(); + + /** + * Sends telemetry data to the endpoint. + * + * @param data Data to send + */ + send(data: object): void { + const queryString = this.serializeQueryParameters(data); + this.fetchImpl + .fetch(`${this.options.endpoint}?${queryString}`, { + method: 'GET', + cache: 'no-store', + keepalive: true, + }) + .catch((error) => { + if (isDevMode()) { + // eslint-disable-next-line no-console + console.error('Failed to send telemetry data', error); + } + }); + } + + /** + * Serializes telemetry data into a query string, + * applying filters and truncating as necessary to fit within the maximum query length. + * + * @param data Telemetry data to serialize + * @returns A query string to be sent to the telemetry endpoint + */ + private serializeQueryParameters(data: object): string { + const { maxQueryLength, truncatedSuffix } = this.options; + const queryString = stringify(data, { + allowDots: true, + arrayFormat: 'indices', + skipNulls: true, + filter: (prefix, value) => serialize(this.applyFilters(prefix, value)), + }); + + return safeTruncateQueryString(queryString, maxQueryLength, { suffix: truncatedSuffix }); + } + + /** + * Applies user-defined filters when serializing telemetry data. + * Each filter is applied sequentially, and shortcircuits as soon as a filter returns a new value. + * + * @param prefix Query parameter key prefix, e.g. "user.id" or "page.url" + * @param value Unserialized value to be sent for this query parameter + * @returns The filtered value, or the original value if no filters apply + */ + private applyFilters(prefix: string, value: unknown): unknown { + for (const filter of this.options.filters) { + const result = filter(prefix, value); + if (result !== value) { + return result; + } + } + + return value; + } +} diff --git a/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/safe-truncate.spec.ts b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/safe-truncate.spec.ts new file mode 100644 index 0000000000..68c8ff7bc3 --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/safe-truncate.spec.ts @@ -0,0 +1,84 @@ +import { safeTruncateQueryString } from './safe-truncate'; + +describe('safeTruncateQueryString', () => { + const longValue = 'a'.repeat(500); + const mediumValue = 'b'.repeat(300); + const shortValue = 'c'.repeat(50); + const suffix = encodeURIComponent('... [truncated]'); + + it('should return unchanged string when already under max length', () => { + const queryString = `key1=${shortValue}&key2=${mediumValue}`; + const result = safeTruncateQueryString(queryString, 1000); + expect(result).toEqual(queryString); + }); + + it('should truncate values to meet max length requirement', () => { + const queryString = `key1=${longValue}&key2=${shortValue}`; + const result = safeTruncateQueryString(queryString, 400); + expect(result.length).toBeLessThanOrEqual(400); + expect(result).toMatch(new RegExp(`key1=a+${suffix}`)); + expect(result).toContain(`key2=${shortValue}`); + }); + + it('should prioritize truncating longest values first', () => { + const queryString = `a=${shortValue}&b=${longValue}&c=${mediumValue}`; + const result = safeTruncateQueryString(queryString, 600); + expect(result.length).toBeLessThanOrEqual(600); + expect(result).toContain(`a=${shortValue}`); + expect(result).toMatch(new RegExp(`b=a+${suffix}`)); + expect(result).toContain(`c=${mediumValue}`); + }); + + it('should handle multiple value truncations', () => { + const queryString = `key1=${longValue}&key2=${longValue}&key3=${longValue}`; + const result = safeTruncateQueryString(queryString, 800); + const truncatedCount = result.match(new RegExp(suffix, 'g'))?.length ?? 0; + expect(result.length).toBeLessThanOrEqual(800); + expect(truncatedCount).toBeGreaterThanOrEqual(2); + }); + + it('should use a custom suffix for truncated values', () => { + const queryString = `key=${longValue}`; + const customSuffix = '[...]'; + const result = safeTruncateQueryString(queryString, 200, { + suffix: customSuffix, + }); + expect(result.length).toBeLessThanOrEqual(200); + expect(result).toMatch(new RegExp(`key=a+${encodeURIComponent(customSuffix)}`)); + }); + + it('should not truncate values below minimum length', () => { + const queryString = `key=${longValue}`; + const minValueLength = 400; + const length = minValueLength + 4 + suffix.length; + const result = safeTruncateQueryString(queryString, length, { minValueLength }); + expect(result.length).toBeLessThanOrEqual(length); + expect(result).toMatch(new RegExp(`key=a{${minValueLength}}${suffix}`)); + }); + + it('should throw error when unable to reach target length', () => { + const queryString = `key=${longValue}`; + const targetLength = 10; + expect(() => safeTruncateQueryString(queryString, targetLength)).toThrow( + `Unable to truncate query string to length ${targetLength}`, + ); + }); + + it('should throw error when unable to reach target length due to minValueLength', () => { + const queryString = `key=${longValue}`; + const minValueLength = 1000; + expect(() => safeTruncateQueryString(queryString, 100, { minValueLength })).toThrow( + `Unable to truncate any more values to minimum length ${minValueLength}`, + ); + }); + + it('should not truncate in the middle of a percent-encoded character', () => { + const queryString = `key=${encodeURIComponent(' '.repeat(300))}`; + const minValueLength = 50; + for (let offset = 0; offset < 3; offset++) { + const result = safeTruncateQueryString(queryString, 200 + offset, { minValueLength }); + expect(result.length).toBeLessThanOrEqual(200 + offset); + expect(result).not.toMatch(/%\w?$/); + } + }); +}); diff --git a/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/safe-truncate.ts b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/safe-truncate.ts new file mode 100644 index 0000000000..0cc9d8bafc --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/safe-truncate.ts @@ -0,0 +1,78 @@ +/** Options for safely truncating a query string */ +export interface SafeTruncateOptions { + /** Suffix to append to truncated values */ + suffix?: string; + /** Minimum length of truncated values (excluding suffix) */ + minValueLength?: number; +} + +/** + * Safely truncates a query string to a maximum length by truncating individual values. + * The input query string is assumed to be URL-encoded. + * + * @param queryString The original query string + * @param maxLength Maximum length of the query string + * @param options Additional options for truncation + * @returns A safely truncated query string + * @throws Error if the query string cannot be truncated to the desired length + */ +export function safeTruncateQueryString( + queryString: string, + maxLength: number, + options: SafeTruncateOptions = {}, +): string { + if (queryString.length <= maxLength) { + return queryString; + } + + const { suffix = '... [truncated]', minValueLength = 100 } = options; + const encodedSuffix = encodeURIComponent(suffix); + const pairs = queryString.split('&'); + // Sort from shortest to longest and use Array#pop to process each pair + const pairsByLength = [...pairs].sort((a, b) => a.length - b.length); + let length = queryString.length; + + while (length > maxLength) { + const current = pairsByLength.pop(); + if (!current) { + throw new Error(`Unable to truncate query string to length ${maxLength}`); + } + + const [key, value] = current.split('='); + if (!value || value.length <= minValueLength + encodedSuffix.length) { + throw new Error(`Unable to truncate any more values to minimum length ${minValueLength}`); + } + + const newValue = safeTruncateQueryValue(value, minValueLength, length - maxLength, encodedSuffix); + const newPair = `${key}=${newValue}`; + const index = pairs.indexOf(current); + pairs[index] = newPair; + length -= current.length - newPair.length; + } + + return pairs.join('&'); +} + +/** + * Safely truncate a query string value to a maximum length by ensuring that URL-encoded characters are not cut in half. + * + * @param value The original value (URL-encoded) + * @param minLength Minimum length of the truncated value (excluding suffix) + * @param truncateLength Desired length to truncate to (excluding suffix) + * @param suffix Suffix to append to the truncated value (URL-encoded) + * @returns A safely truncated value with the suffix appended + */ +function safeTruncateQueryValue(value: string, minLength: number, truncateLength: number, suffix: string): string { + const candidateLength = Math.max(minLength, value.length - suffix.length - truncateLength); + const lastEncodedIndex = value.lastIndexOf('%', candidateLength); + let length: number; + if (lastEncodedIndex < 0 || candidateLength - lastEncodedIndex > 2) { + length = candidateLength; + } else if (candidateLength === minLength) { + length = lastEncodedIndex + 3; + } else { + length = lastEncodedIndex; + } + + return value.slice(0, length) + suffix; +} diff --git a/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/serialize.spec.ts b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/serialize.spec.ts new file mode 100644 index 0000000000..59b9942f3d --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/serialize.spec.ts @@ -0,0 +1,97 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { serialize } from './serialize'; + +function createAnchorElement(attrs: Record): HTMLAnchorElement { + const el = document.createElement('a'); + for (const [attr, value] of Object.entries(attrs)) { + el.setAttribute(attr, value); + } + + return el; +} + +describe('serialize(value)', () => { + it('should return primitive (including null) values unchanged', () => { + expect(serialize('a')).toEqual('a'); + expect(serialize(1)).toEqual(1); + expect(serialize(false)).toEqual(false); + expect(serialize(-1n)).toEqual(-1n); + expect(serialize(undefined)).toEqual(undefined); + expect(serialize(null)).toEqual(null); + }); + + it('should serialize dates into ISO strings', () => { + const date = new Date(0); + expect(serialize(date)).toEqual(date.toISOString()); + }); + + it('should serialize errors into plain objects', () => { + const msg = 'my error message'; + const error = new Error(msg); + expect(serialize(error)).toEqual({ message: msg, name: error.name, stack: expect.any(String) }); + + const error2 = new TypeError(msg); + expect(serialize(error2)).toEqual({ message: msg, name: error2.name, stack: expect.any(String) }); + }); + + it('should serialize events into plain objects', () => { + const msg = 'unexpected error'; + const file = 'test.ts'; + const event = new ErrorEvent('error', { message: msg, filename: file, lineno: 5, colno: 10 }); + expect(serialize(event)).toEqual({ message: msg, filename: file, lineno: 5, colno: 10 }); + + const event2 = new KeyboardEvent('pressed', { key: 'q', shiftKey: true }); + expect(serialize(event2)).toEqual({ key: 'q', shiftKey: true }); + + const event3 = new MouseEvent('click', { button: 0, buttons: 2, ctrlKey: true }); + expect(serialize(event3)).toEqual({ button: 0, buttons: 2, ctrlKey: true }); + + const href = 'https://example.com'; + const anchorEl4 = createAnchorElement({ href, target: '_blank' }); + const event4 = Object.defineProperty(new MouseEvent('click'), 'currentTarget', { value: anchorEl4 }); + expect(serialize(event4)).toMatchObject({ href, target: '_blank' }); + + const anchorEl5 = createAnchorElement({ href, download: 'file.csv', type: 'text/csv' }); + const event5 = Object.defineProperty(new MouseEvent('click'), 'currentTarget', { value: anchorEl5 }); + const result5 = serialize(event5); + expect(result5).toMatchObject({ href, download: 'file.csv', type: 'text/csv' }); + expect(result5).not.toMatchObject({ target: expect.anything() }); + + const event6 = new CustomEvent('custom'); + expect(serialize(event6)).toEqual({ type: 'custom' }); + }); + + it('should serialize maps into an object containing an array of entries', () => { + const entries = [ + [1, 2], + [3, 4], + ] as const; + expect(serialize(new Map(entries))).toEqual({ map: entries }); + }); + + it('should serialize sets into an object containing an array of values', () => { + const values = [1, 'a', true] as const; + expect(serialize(new Set(values))).toEqual({ set: values }); + }); + + it('should serialize HttpErrorResponse into plain objects', () => { + const response = new HttpErrorResponse({ + status: 404, + url: 'https://example.com/resource', + statusText: 'Not Found', + error: 'Resource not found', + }); + + expect(serialize(response)).toEqual({ + status: 404, + url: 'https://example.com/resource', + message: 'Http failure response for https://example.com/resource: 404 Not Found', + error: 'Resource not found', + }); + }); + + it('should return other objects unchanged', () => { + const obj = { a: 1, b: 'two', c: true }; + expect(serialize(obj)).toBe(obj); + }); +}); diff --git a/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/serialize.ts b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/serialize.ts new file mode 100644 index 0000000000..60afd76810 --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-analytics/src/lib/util/serialize.ts @@ -0,0 +1,108 @@ +import { HttpErrorResponse } from '@angular/common/http'; + +/** + * Serializes complex values into simpler types. + * Currently handles the following objects: + * - Errors (and subclasses) + * - Events (ErrorEvent, KeyboardEvent, and MouseEvent) + * - Maps + * - Sets + * - Angular's HTTP error responses + * + * @param value Value to serialize + * @returns A replacement value to serialize + */ +export function serialize(value: unknown): unknown { + // Short circuit for non-objects + if (typeof value !== 'object' || value === null) { + return value; + } + + if (value instanceof Date) { + return value.toISOString(); + } else if (value instanceof Error) { + return pick(value, ['name', 'message', 'stack']); + } else if (value instanceof HttpErrorResponse) { + return pick(value, ['status', 'url', 'message', 'error']); + } else if (value instanceof Event) { + if (value instanceof ErrorEvent) { + return pick(value, ['message', 'filename', 'lineno', 'colno']); + } else if (value instanceof KeyboardEvent) { + const keys: (keyof KeyboardEvent)[] = ['key', 'altKey', 'ctrlKey', 'metaKey', 'shiftKey', 'repeat']; + return filterFalse(pick(value, keys)); + } else if (value instanceof MouseEvent) { + const keys: (keyof MouseEvent)[] = ['button', 'buttons', 'altKey', 'ctrlKey', 'metaKey', 'shiftKey']; + const props = pick(value, keys); + + const { type, currentTarget: el } = value; + const isAnchorClick = type === 'click' && el instanceof HTMLAnchorElement; + const targetKeys: (keyof HTMLAnchorElement)[] = ['href', 'type', 'target', 'download']; + const targetProps = isAnchorClick ? pickAttributes(el, targetKeys) : {}; + + return filterFalse({ ...props, ...targetProps }); + } + + return pick(value, ['type']); + } else if (value instanceof Map) { + return { map: [...value] }; + } else if (value instanceof Set) { + return { set: [...value] }; + } + + return value; +} + +/** + * Pick a set of properties from an object + * + * @param obj Original object + * @param keys Keys to pick from the object + * @returns A subset of the object + */ +function pick(obj: T, keys: K[]): Pick { + return keys.reduce( + (acc, key) => { + acc[key] = obj[key]; + return acc; + }, + {} as Pick, + ); +} + +/** + * Pick a set of attributes from an element. + * Excludes attributes not present on the element. + * + * @param el Element reference + * @param attributes Attributes to pick + * @returns Values by attribute name + */ +function pickAttributes(el: T, attributes: K[]): Partial> { + return attributes.reduce( + (acc, attr) => { + const value = el.getAttribute(attr); + if (value !== null) { + acc[attr] = value; + } + return acc; + }, + {} as Partial>, + ); +} + +/** + * Creates a new object where any keys with a value strictly equal to false are removed + * + * @param obj Original object + * @returns Filtered object + */ +function filterFalse(obj: T): Partial { + const result: Partial = {}; + for (const key in obj) { + if (obj[key] !== false) { + result[key] = obj[key]; + } + } + + return result; +} diff --git a/libs/hra-ui/analytics/plugins/hra-event-filter/ng-package.json b/libs/hra-ui/analytics/plugins/hra-event-filter/ng-package.json new file mode 100644 index 0000000000..c781f0df46 --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-event-filter/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/hra-ui/analytics/plugins/hra-event-filter/src/index.ts b/libs/hra-ui/analytics/plugins/hra-event-filter/src/index.ts new file mode 100644 index 0000000000..d480374ca1 --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-event-filter/src/index.ts @@ -0,0 +1 @@ +export { hraEventFilterPlugin, HraEventFilterPluginConfig } from './lib/plugin'; diff --git a/libs/hra-ui/analytics/plugins/hra-event-filter/src/lib/plugin.spec.ts b/libs/hra-ui/analytics/plugins/hra-event-filter/src/lib/plugin.spec.ts new file mode 100644 index 0000000000..21a5719745 --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-event-filter/src/lib/plugin.spec.ts @@ -0,0 +1,56 @@ +import { PLATFORM_ID } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { CoreEvents } from '@hra-ui/common/analytics/events'; +import { hraEventFilterPlugin } from './plugin'; + +describe('hra-event-filter plugin', () => { + function setup(isBrowser = true) { + TestBed.configureTestingModule({ + providers: [ + { + provide: PLATFORM_ID, + useValue: isBrowser ? 'browser' : 'server', + }, + ], + }); + + const abort = jest.fn(); + const isEventEnabled = jest.fn().mockReturnValue(false); + const plugin = TestBed.runInInjectionContext(() => hraEventFilterPlugin({ isEventEnabled })); + const callPluginMethod = (method: 'pageStart' | 'identifyStart' | 'trackStart'): void => { + (plugin[method] as (data: unknown) => void)({ + abort, + payload: { + event: CoreEvents.Click.type, + options: { category: CoreEvents.Click.category }, + }, + }); + }; + + return { abort, callPluginMethod, isEventEnabled, plugin }; + } + + it('should abort when an event is disabled', () => { + const { abort, callPluginMethod } = setup(); + callPluginMethod('pageStart'); + callPluginMethod('identifyStart'); + callPluginMethod('trackStart'); + expect(abort).toHaveBeenCalledTimes(3); + }); + + it('should abort when not running in the browser', () => { + const { abort, callPluginMethod, isEventEnabled } = setup(false); + isEventEnabled.mockReturnValue(true); + callPluginMethod('pageStart'); + callPluginMethod('identifyStart'); + callPluginMethod('trackStart'); + expect(abort).toHaveBeenCalledTimes(3); + }); + + it('should not call abort when the event is enabled', () => { + const { abort, callPluginMethod, isEventEnabled } = setup(); + isEventEnabled.mockReturnValueOnce(true); + callPluginMethod('pageStart'); + expect(abort).not.toHaveBeenCalled(); + }); +}); diff --git a/libs/hra-ui/analytics/plugins/hra-event-filter/src/lib/plugin.ts b/libs/hra-ui/analytics/plugins/hra-event-filter/src/lib/plugin.ts new file mode 100644 index 0000000000..5132279461 --- /dev/null +++ b/libs/hra-ui/analytics/plugins/hra-event-filter/src/lib/plugin.ts @@ -0,0 +1,69 @@ +import { isPlatformBrowser } from '@angular/common'; +import { inject, Injector, PLATFORM_ID } from '@angular/core'; +import { CoreEvents, EventCategory, EventType } from '@hra-ui/analytics/events'; +import { AnalyticsPlugin } from 'analytics'; +import { assertInjector } from 'ngxtension/assert-injector'; + +/** Plugin configuration */ +export interface HraEventFilterPluginConfig { + /** Callback to check whether an event is enabled */ + isEventEnabled: (type: EventType, category?: EventCategory) => boolean; + /** Plugin injector context */ + injector?: Injector; +} + +/** Abort function */ +type AbortFn = (reason: string) => unknown; + +/** + * Interface for data passed by the 'analytics' library to plugin callbacks. + * Only declares the fields that are used by the hra-event-filter plugin. + */ +interface EventData { + /** Aborts the current event */ + abort: AbortFn; + /** Payload data */ + payload: { + /** Event type */ + event: EventType; + /** Event options */ + options: { + /** Event category */ + category?: EventCategory; + }; + }; +} + +/** + * An `analytics` plugin that filters events based on an `isEventEnabled` callback + * + * @param config Plugin configuration + * @returns An analytics plugin + */ +export function hraEventFilterPlugin(config: HraEventFilterPluginConfig): AnalyticsPlugin { + return assertInjector(hraEventFilterPlugin, config.injector, () => { + const isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); + const { isEventEnabled } = config; + const abortIfDisabled = (type: EventType, category: EventCategory | undefined, abort: AbortFn) => { + if (!isBrowser || !isEventEnabled(type, category)) { + return abort(`Event '${type}' is disabled`); + } + return undefined; + }; + + return { + name: 'hra-event-filter', + config: {}, + pageStart({ abort }: EventData): unknown { + const { type, category } = CoreEvents.PageView; + return abortIfDisabled(type, category, abort); + }, + identifyStart({ abort }: EventData): unknown { + return abort('Identify disabled'); + }, + trackStart({ abort, payload }: EventData): unknown { + return abortIfDisabled(payload.event, payload.options.category, abort); + }, + }; + }); +} diff --git a/libs/hra-ui/analytics/project.json b/libs/hra-ui/analytics/project.json new file mode 100644 index 0000000000..a53ecb946a --- /dev/null +++ b/libs/hra-ui/analytics/project.json @@ -0,0 +1,19 @@ +{ + "name": "analytics", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/hra-ui/analytics/src", + "prefix": "hra", + "projectType": "library", + "tags": ["type:lib", "project:analytics"], + "targets": { + "build": { + "executor": "@nx/angular:package" + }, + "compodoc": { + "executor": "@twittwer/compodoc:compodoc", + "options": { + "tsConfig": "libs/hra-ui/analytics/tsconfig.lib.json" + } + } + } +} diff --git a/libs/hra-ui/analytics/src/index.ts b/libs/hra-ui/analytics/src/index.ts new file mode 100644 index 0000000000..13e17abbbf --- /dev/null +++ b/libs/hra-ui/analytics/src/index.ts @@ -0,0 +1,2 @@ +/** A placeholder value until code is moved to this library */ +export const placeholder = 'placeholder'; diff --git a/libs/hra-ui/analytics/src/lib/.gitkeep b/libs/hra-ui/analytics/src/lib/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/hra-ui/analytics/src/test-setup.ts b/libs/hra-ui/analytics/src/test-setup.ts new file mode 100644 index 0000000000..1928dfb03e --- /dev/null +++ b/libs/hra-ui/analytics/src/test-setup.ts @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom'; +import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; + +setupZonelessTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/hra-ui/analytics/tsconfig.json b/libs/hra-ui/analytics/tsconfig.json new file mode 100644 index 0000000000..bef7691262 --- /dev/null +++ b/libs/hra-ui/analytics/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": {}, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/hra-ui/analytics/tsconfig.lib.json b/libs/hra-ui/analytics/tsconfig.lib.json new file mode 100644 index 0000000000..1932a18fb7 --- /dev/null +++ b/libs/hra-ui/analytics/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "**/*.test.ts"] +} diff --git a/libs/shared/utils/tsconfig.lib.prod.json b/libs/hra-ui/analytics/tsconfig.lib.prod.json similarity index 100% rename from libs/shared/utils/tsconfig.lib.prod.json rename to libs/hra-ui/analytics/tsconfig.lib.prod.json diff --git a/libs/shared/utils/tsconfig.spec.json b/libs/hra-ui/analytics/tsconfig.spec.json similarity index 57% rename from libs/shared/utils/tsconfig.spec.json rename to libs/hra-ui/analytics/tsconfig.spec.json index 2156ddccc0..2b98a4e8c5 100644 --- a/libs/shared/utils/tsconfig.spec.json +++ b/libs/hra-ui/analytics/tsconfig.spec.json @@ -2,9 +2,9 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../../dist/out-tsc", - "types": ["jest", "node"], + "types": ["jest", "node", "jest-dom"], "isolatedModules": true }, "files": ["src/test-setup.ts"], - "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/libs/hra-ui/application/README.md b/libs/hra-ui/application/README.md new file mode 100644 index 0000000000..735a60668d --- /dev/null +++ b/libs/hra-ui/application/README.md @@ -0,0 +1,7 @@ +# application + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test application` to execute the unit tests. diff --git a/libs/hra-ui/application/eslint.config.mjs b/libs/hra-ui/application/eslint.config.mjs new file mode 100644 index 0000000000..40fe3e29eb --- /dev/null +++ b/libs/hra-ui/application/eslint.config.mjs @@ -0,0 +1,3 @@ +import { configs } from '../../../eslint.config.mjs'; + +export default [...configs.base, ...configs.lib, ...configs.angular]; diff --git a/libs/hra-ui/application/jest.config.ts b/libs/hra-ui/application/jest.config.ts new file mode 100644 index 0000000000..a638c6035e --- /dev/null +++ b/libs/hra-ui/application/jest.config.ts @@ -0,0 +1,6 @@ +export default { + displayName: 'application', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/hra-ui/application', +}; diff --git a/libs/hra-ui/application/ng-package.json b/libs/hra-ui/application/ng-package.json new file mode 100644 index 0000000000..46cfa909ee --- /dev/null +++ b/libs/hra-ui/application/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/hra-ui/application", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/hra-ui/application/package.json b/libs/hra-ui/application/package.json new file mode 100644 index 0000000000..82d4d31c85 --- /dev/null +++ b/libs/hra-ui/application/package.json @@ -0,0 +1,6 @@ +{ + "name": "@hra-ui/application-v2", + "version": "0.0.1", + "peerDependencies": {}, + "sideEffects": false +} diff --git a/libs/hra-ui/application/project.json b/libs/hra-ui/application/project.json new file mode 100644 index 0000000000..bdc8a68925 --- /dev/null +++ b/libs/hra-ui/application/project.json @@ -0,0 +1,19 @@ +{ + "name": "application-v2", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/hra-ui/application/src", + "prefix": "hra", + "projectType": "library", + "tags": ["type:lib", "project:application"], + "targets": { + "build": { + "executor": "@nx/angular:package" + }, + "compodoc": { + "executor": "@twittwer/compodoc:compodoc", + "options": { + "tsConfig": "libs/hra-ui/application/tsconfig.lib.json" + } + } + } +} diff --git a/libs/hra-ui/application/src/index.ts b/libs/hra-ui/application/src/index.ts new file mode 100644 index 0000000000..13e17abbbf --- /dev/null +++ b/libs/hra-ui/application/src/index.ts @@ -0,0 +1,2 @@ +/** A placeholder value until code is moved to this library */ +export const placeholder = 'placeholder'; diff --git a/libs/hra-ui/application/src/lib/.gitkeep b/libs/hra-ui/application/src/lib/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/hra-ui/application/src/test-setup.ts b/libs/hra-ui/application/src/test-setup.ts new file mode 100644 index 0000000000..1928dfb03e --- /dev/null +++ b/libs/hra-ui/application/src/test-setup.ts @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom'; +import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; + +setupZonelessTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/hra-ui/application/tsconfig.json b/libs/hra-ui/application/tsconfig.json new file mode 100644 index 0000000000..bef7691262 --- /dev/null +++ b/libs/hra-ui/application/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": {}, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/hra-ui/application/tsconfig.lib.json b/libs/hra-ui/application/tsconfig.lib.json new file mode 100644 index 0000000000..1932a18fb7 --- /dev/null +++ b/libs/hra-ui/application/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "**/*.test.ts"] +} diff --git a/libs/hra-ui/application/tsconfig.lib.prod.json b/libs/hra-ui/application/tsconfig.lib.prod.json new file mode 100644 index 0000000000..2a2faa884c --- /dev/null +++ b/libs/hra-ui/application/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/libs/hra-ui/application/tsconfig.spec.json b/libs/hra-ui/application/tsconfig.spec.json new file mode 100644 index 0000000000..2b98a4e8c5 --- /dev/null +++ b/libs/hra-ui/application/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["jest", "node", "jest-dom"], + "isolatedModules": true + }, + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/hra-ui/cdk/README.md b/libs/hra-ui/cdk/README.md new file mode 100644 index 0000000000..2eb7d4515f --- /dev/null +++ b/libs/hra-ui/cdk/README.md @@ -0,0 +1,7 @@ +# cdk + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test cdk` to execute the unit tests. diff --git a/libs/hra-ui/cdk/eslint.config.mjs b/libs/hra-ui/cdk/eslint.config.mjs new file mode 100644 index 0000000000..40fe3e29eb --- /dev/null +++ b/libs/hra-ui/cdk/eslint.config.mjs @@ -0,0 +1,3 @@ +import { configs } from '../../../eslint.config.mjs'; + +export default [...configs.base, ...configs.lib, ...configs.angular]; diff --git a/libs/shared/utils/jest.config.ts b/libs/hra-ui/cdk/jest.config.ts similarity index 55% rename from libs/shared/utils/jest.config.ts rename to libs/hra-ui/cdk/jest.config.ts index d6546d0e4c..3e2112e7ea 100644 --- a/libs/shared/utils/jest.config.ts +++ b/libs/hra-ui/cdk/jest.config.ts @@ -1,6 +1,6 @@ export default { - displayName: 'shared-utils', + displayName: 'cdk', preset: '../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], - coverageDirectory: '../../../coverage/libs/shared/utils', + coverageDirectory: '../../../coverage/libs/hra-ui/cdk', }; diff --git a/libs/shared/utils/ng-package.json b/libs/hra-ui/cdk/ng-package.json similarity index 73% rename from libs/shared/utils/ng-package.json rename to libs/hra-ui/cdk/ng-package.json index 173d6ce11e..f9a26f0a65 100644 --- a/libs/shared/utils/ng-package.json +++ b/libs/hra-ui/cdk/ng-package.json @@ -1,6 +1,6 @@ { "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../../dist/libs/shared/utils", + "dest": "../../../dist/libs/hra-ui/cdk", "lib": { "entryFile": "src/index.ts" } diff --git a/libs/hra-ui/cdk/package.json b/libs/hra-ui/cdk/package.json new file mode 100644 index 0000000000..4343667ae5 --- /dev/null +++ b/libs/hra-ui/cdk/package.json @@ -0,0 +1,6 @@ +{ + "name": "@hra-ui/cdk-v2", + "version": "0.0.1", + "peerDependencies": {}, + "sideEffects": false +} diff --git a/libs/hra-ui/cdk/project.json b/libs/hra-ui/cdk/project.json new file mode 100644 index 0000000000..ab95100242 --- /dev/null +++ b/libs/hra-ui/cdk/project.json @@ -0,0 +1,19 @@ +{ + "name": "cdk-v2", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/hra-ui/cdk/src", + "prefix": "hra", + "projectType": "library", + "tags": ["type:lib", "project:cdk"], + "targets": { + "build": { + "executor": "@nx/angular:package" + }, + "compodoc": { + "executor": "@twittwer/compodoc:compodoc", + "options": { + "tsConfig": "libs/hra-ui/cdk/tsconfig.lib.json" + } + } + } +} diff --git a/libs/hra-ui/cdk/src/index.ts b/libs/hra-ui/cdk/src/index.ts new file mode 100644 index 0000000000..13e17abbbf --- /dev/null +++ b/libs/hra-ui/cdk/src/index.ts @@ -0,0 +1,2 @@ +/** A placeholder value until code is moved to this library */ +export const placeholder = 'placeholder'; diff --git a/libs/hra-ui/cdk/src/lib/.gitkeep b/libs/hra-ui/cdk/src/lib/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/hra-ui/cdk/src/test-setup.ts b/libs/hra-ui/cdk/src/test-setup.ts new file mode 100644 index 0000000000..1928dfb03e --- /dev/null +++ b/libs/hra-ui/cdk/src/test-setup.ts @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom'; +import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; + +setupZonelessTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/hra-ui/cdk/tsconfig.json b/libs/hra-ui/cdk/tsconfig.json new file mode 100644 index 0000000000..bef7691262 --- /dev/null +++ b/libs/hra-ui/cdk/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": {}, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/hra-ui/cdk/tsconfig.lib.json b/libs/hra-ui/cdk/tsconfig.lib.json new file mode 100644 index 0000000000..1932a18fb7 --- /dev/null +++ b/libs/hra-ui/cdk/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "**/*.test.ts"] +} diff --git a/libs/hra-ui/cdk/tsconfig.lib.prod.json b/libs/hra-ui/cdk/tsconfig.lib.prod.json new file mode 100644 index 0000000000..2a2faa884c --- /dev/null +++ b/libs/hra-ui/cdk/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/libs/hra-ui/cdk/tsconfig.spec.json b/libs/hra-ui/cdk/tsconfig.spec.json new file mode 100644 index 0000000000..2b98a4e8c5 --- /dev/null +++ b/libs/hra-ui/cdk/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["jest", "node", "jest-dom"], + "isolatedModules": true + }, + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/hra-ui/common/README.md b/libs/hra-ui/common/README.md new file mode 100644 index 0000000000..dd1a64c606 --- /dev/null +++ b/libs/hra-ui/common/README.md @@ -0,0 +1,7 @@ +# common + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test common` to execute the unit tests. diff --git a/libs/hra-ui/common/eslint.config.mjs b/libs/hra-ui/common/eslint.config.mjs new file mode 100644 index 0000000000..40fe3e29eb --- /dev/null +++ b/libs/hra-ui/common/eslint.config.mjs @@ -0,0 +1,3 @@ +import { configs } from '../../../eslint.config.mjs'; + +export default [...configs.base, ...configs.lib, ...configs.angular]; diff --git a/libs/hra-ui/common/jest.config.ts b/libs/hra-ui/common/jest.config.ts new file mode 100644 index 0000000000..e116d6dae1 --- /dev/null +++ b/libs/hra-ui/common/jest.config.ts @@ -0,0 +1,6 @@ +export default { + displayName: 'common', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/hra-ui/common', +}; diff --git a/libs/hra-ui/common/ng-package.json b/libs/hra-ui/common/ng-package.json new file mode 100644 index 0000000000..ac49fe782e --- /dev/null +++ b/libs/hra-ui/common/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/hra-ui/common", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/hra-ui/common/package.json b/libs/hra-ui/common/package.json new file mode 100644 index 0000000000..aafaf84ef8 --- /dev/null +++ b/libs/hra-ui/common/package.json @@ -0,0 +1,6 @@ +{ + "name": "@hra-ui/common-v2", + "version": "0.0.1", + "peerDependencies": {}, + "sideEffects": false +} diff --git a/libs/hra-ui/common/project.json b/libs/hra-ui/common/project.json new file mode 100644 index 0000000000..870eb31809 --- /dev/null +++ b/libs/hra-ui/common/project.json @@ -0,0 +1,19 @@ +{ + "name": "common-v2", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/hra-ui/common/src", + "prefix": "hra", + "projectType": "library", + "tags": ["type:lib", "project:common"], + "targets": { + "build": { + "executor": "@nx/angular:package" + }, + "compodoc": { + "executor": "@twittwer/compodoc:compodoc", + "options": { + "tsConfig": "libs/hra-ui/common/tsconfig.lib.json" + } + } + } +} diff --git a/libs/hra-ui/common/src/index.ts b/libs/hra-ui/common/src/index.ts new file mode 100644 index 0000000000..13e17abbbf --- /dev/null +++ b/libs/hra-ui/common/src/index.ts @@ -0,0 +1,2 @@ +/** A placeholder value until code is moved to this library */ +export const placeholder = 'placeholder'; diff --git a/libs/hra-ui/common/src/lib/.gitkeep b/libs/hra-ui/common/src/lib/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/hra-ui/common/src/lib/injectors/app-configuration.spec.ts b/libs/hra-ui/common/src/lib/injectors/app-configuration.spec.ts new file mode 100644 index 0000000000..8f12565c17 --- /dev/null +++ b/libs/hra-ui/common/src/lib/injectors/app-configuration.spec.ts @@ -0,0 +1,21 @@ +import { TestBed } from '@angular/core/testing'; +import { AppConfiguration, injectAppConfiguration, provideAppConfiguration } from './app-configuration'; + +describe('AppConfiguration', () => { + it('should provide custom configuration', () => { + const customConfig: AppConfiguration = { + name: 'Test App', + version: '1.0.0', + url: 'https://test.app', + }; + + TestBed.configureTestingModule({ + providers: [provideAppConfiguration(customConfig)], + }); + + TestBed.runInInjectionContext(() => { + const config = injectAppConfiguration(); + expect(config).toEqual(customConfig); + }); + }); +}); diff --git a/libs/hra-ui/common/src/lib/injectors/app-configuration.ts b/libs/hra-ui/common/src/lib/injectors/app-configuration.ts new file mode 100644 index 0000000000..b2591acac7 --- /dev/null +++ b/libs/hra-ui/common/src/lib/injectors/app-configuration.ts @@ -0,0 +1,20 @@ +import { createInjectionToken } from 'ngxtension/create-injection-token'; + +/** Application configuration */ +export interface AppConfiguration { + /** Application name */ + name?: string; + /** Application version */ + version?: string; + /** Application url */ + url?: string; +} + +/** Application configuration */ +const APP_CONFIGURATION = createInjectionToken((): AppConfiguration => ({})); + +/** Inject the global application configuration */ +export const injectAppConfiguration = APP_CONFIGURATION[0]; + +/** Set the application configuration */ +export const provideAppConfiguration = APP_CONFIGURATION[1]; diff --git a/libs/hra-ui/common/src/lib/injectors/document.spec.ts b/libs/hra-ui/common/src/lib/injectors/document.spec.ts new file mode 100644 index 0000000000..7523d46034 --- /dev/null +++ b/libs/hra-ui/common/src/lib/injectors/document.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; +import { DOCUMENT } from '@angular/common'; +import { injectDocument } from './document'; + +describe('Document Injector', () => { + it('should inject the document object', () => { + const mockDocument = { title: 'Test Document' } as Document; + + TestBed.configureTestingModule({ + providers: [{ provide: DOCUMENT, useValue: mockDocument }], + }); + + TestBed.runInInjectionContext(() => { + const document = injectDocument(); + expect(document).toBe(mockDocument); + expect(document.title).toBe('Test Document'); + }); + }); +}); diff --git a/libs/hra-ui/common/src/lib/injectors/document.ts b/libs/hra-ui/common/src/lib/injectors/document.ts new file mode 100644 index 0000000000..93df1e9ced --- /dev/null +++ b/libs/hra-ui/common/src/lib/injectors/document.ts @@ -0,0 +1,10 @@ +import { DOCUMENT, inject } from '@angular/core'; +import { createInjectionToken } from 'ngxtension/create-injection-token'; + +/** Document injection token */ +const DOCUMENT_TOKEN = createInjectionToken(() => inject(DOCUMENT)); + +/** Inject the global document object */ +export const injectDocument = DOCUMENT_TOKEN[0]; +/** Provide a different global document object. For testing only! */ +export const provideDocument = DOCUMENT_TOKEN[1]; diff --git a/libs/hra-ui/common/src/lib/injectors/window.spec.ts b/libs/hra-ui/common/src/lib/injectors/window.spec.ts new file mode 100644 index 0000000000..30d7a336aa --- /dev/null +++ b/libs/hra-ui/common/src/lib/injectors/window.spec.ts @@ -0,0 +1,36 @@ +import { TestBed } from '@angular/core/testing'; +import { DOCUMENT } from '@angular/common'; +import { injectWindow } from './window'; + +describe('Window Injector', () => { + it('should inject window from document.defaultView', () => { + const mockWindow = { location: { href: 'https://test.com' } } as Window; + const mockDocument = { + defaultView: mockWindow, + } as Document; + + TestBed.configureTestingModule({ + providers: [{ provide: DOCUMENT, useValue: mockDocument }], + }); + + TestBed.runInInjectionContext(() => { + const windowObj = injectWindow(); + expect(windowObj).toBe(mockWindow); + }); + }); + + it('should fallback to global window when defaultView is null', () => { + const mockDocument = { + defaultView: null, + } as Document; + + TestBed.configureTestingModule({ + providers: [{ provide: DOCUMENT, useValue: mockDocument }], + }); + + TestBed.runInInjectionContext(() => { + const windowObj = injectWindow(); + expect(windowObj).toBe(window); + }); + }); +}); diff --git a/libs/hra-ui/common/src/lib/injectors/window.ts b/libs/hra-ui/common/src/lib/injectors/window.ts new file mode 100644 index 0000000000..5dd3d5510b --- /dev/null +++ b/libs/hra-ui/common/src/lib/injectors/window.ts @@ -0,0 +1,9 @@ +import { createInjectionToken } from 'ngxtension/create-injection-token'; +import { injectDocument } from './document'; + +/** Window injection token */ +const WINDOW_TOKEN = createInjectionToken(() => injectDocument().defaultView ?? window); + +export const injectWindow = WINDOW_TOKEN[0]; +/** Provide a new global window object. For testing only! */ +export const provideWindow = WINDOW_TOKEN[1]; diff --git a/libs/hra-ui/common/src/test-setup.ts b/libs/hra-ui/common/src/test-setup.ts new file mode 100644 index 0000000000..1928dfb03e --- /dev/null +++ b/libs/hra-ui/common/src/test-setup.ts @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom'; +import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; + +setupZonelessTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/hra-ui/common/tsconfig.json b/libs/hra-ui/common/tsconfig.json new file mode 100644 index 0000000000..bef7691262 --- /dev/null +++ b/libs/hra-ui/common/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": {}, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/hra-ui/common/tsconfig.lib.json b/libs/hra-ui/common/tsconfig.lib.json new file mode 100644 index 0000000000..1932a18fb7 --- /dev/null +++ b/libs/hra-ui/common/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "**/*.test.ts"] +} diff --git a/libs/hra-ui/common/tsconfig.lib.prod.json b/libs/hra-ui/common/tsconfig.lib.prod.json new file mode 100644 index 0000000000..2a2faa884c --- /dev/null +++ b/libs/hra-ui/common/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/libs/hra-ui/common/tsconfig.spec.json b/libs/hra-ui/common/tsconfig.spec.json new file mode 100644 index 0000000000..2b98a4e8c5 --- /dev/null +++ b/libs/hra-ui/common/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["jest", "node", "jest-dom"], + "isolatedModules": true + }, + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/hra-ui/content-templates/README.md b/libs/hra-ui/content-templates/README.md new file mode 100644 index 0000000000..aa5cbc5c92 --- /dev/null +++ b/libs/hra-ui/content-templates/README.md @@ -0,0 +1,7 @@ +# content-templates + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test content-templates` to execute the unit tests. diff --git a/libs/hra-ui/content-templates/eslint.config.mjs b/libs/hra-ui/content-templates/eslint.config.mjs new file mode 100644 index 0000000000..40fe3e29eb --- /dev/null +++ b/libs/hra-ui/content-templates/eslint.config.mjs @@ -0,0 +1,3 @@ +import { configs } from '../../../eslint.config.mjs'; + +export default [...configs.base, ...configs.lib, ...configs.angular]; diff --git a/libs/hra-ui/content-templates/jest.config.ts b/libs/hra-ui/content-templates/jest.config.ts new file mode 100644 index 0000000000..899d38d7fd --- /dev/null +++ b/libs/hra-ui/content-templates/jest.config.ts @@ -0,0 +1,6 @@ +export default { + displayName: 'content-templates', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/hra-ui/content-templates', +}; diff --git a/libs/hra-ui/content-templates/ng-package.json b/libs/hra-ui/content-templates/ng-package.json new file mode 100644 index 0000000000..1c3da62ea4 --- /dev/null +++ b/libs/hra-ui/content-templates/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/hra-ui/content-templates", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/hra-ui/content-templates/package.json b/libs/hra-ui/content-templates/package.json new file mode 100644 index 0000000000..d9631db9da --- /dev/null +++ b/libs/hra-ui/content-templates/package.json @@ -0,0 +1,6 @@ +{ + "name": "@hra-ui/content-templates", + "version": "0.0.1", + "peerDependencies": {}, + "sideEffects": false +} diff --git a/libs/hra-ui/content-templates/project.json b/libs/hra-ui/content-templates/project.json new file mode 100644 index 0000000000..f7420787e0 --- /dev/null +++ b/libs/hra-ui/content-templates/project.json @@ -0,0 +1,19 @@ +{ + "name": "content-templates", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/hra-ui/content-templates/src", + "prefix": "hra", + "projectType": "library", + "tags": ["type:lib", "project:content-templates"], + "targets": { + "build": { + "executor": "@nx/angular:package" + }, + "compodoc": { + "executor": "@twittwer/compodoc:compodoc", + "options": { + "tsConfig": "libs/hra-ui/content-templates/tsconfig.lib.json" + } + } + } +} diff --git a/libs/hra-ui/content-templates/src/index.ts b/libs/hra-ui/content-templates/src/index.ts new file mode 100644 index 0000000000..13e17abbbf --- /dev/null +++ b/libs/hra-ui/content-templates/src/index.ts @@ -0,0 +1,2 @@ +/** A placeholder value until code is moved to this library */ +export const placeholder = 'placeholder'; diff --git a/libs/hra-ui/content-templates/src/lib/.gitkeep b/libs/hra-ui/content-templates/src/lib/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/hra-ui/content-templates/src/test-setup.ts b/libs/hra-ui/content-templates/src/test-setup.ts new file mode 100644 index 0000000000..1928dfb03e --- /dev/null +++ b/libs/hra-ui/content-templates/src/test-setup.ts @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom'; +import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; + +setupZonelessTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/hra-ui/content-templates/tsconfig.json b/libs/hra-ui/content-templates/tsconfig.json new file mode 100644 index 0000000000..bef7691262 --- /dev/null +++ b/libs/hra-ui/content-templates/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": {}, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/hra-ui/content-templates/tsconfig.lib.json b/libs/hra-ui/content-templates/tsconfig.lib.json new file mode 100644 index 0000000000..1932a18fb7 --- /dev/null +++ b/libs/hra-ui/content-templates/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "**/*.test.ts"] +} diff --git a/libs/hra-ui/content-templates/tsconfig.lib.prod.json b/libs/hra-ui/content-templates/tsconfig.lib.prod.json new file mode 100644 index 0000000000..2a2faa884c --- /dev/null +++ b/libs/hra-ui/content-templates/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/libs/hra-ui/content-templates/tsconfig.spec.json b/libs/hra-ui/content-templates/tsconfig.spec.json new file mode 100644 index 0000000000..2b98a4e8c5 --- /dev/null +++ b/libs/hra-ui/content-templates/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["jest", "node", "jest-dom"], + "isolatedModules": true + }, + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/hra-ui/design-system/README.md b/libs/hra-ui/design-system/README.md new file mode 100644 index 0000000000..3369998942 --- /dev/null +++ b/libs/hra-ui/design-system/README.md @@ -0,0 +1,7 @@ +# design-system + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test design-system` to execute the unit tests. diff --git a/libs/hra-ui/design-system/eslint.config.mjs b/libs/hra-ui/design-system/eslint.config.mjs new file mode 100644 index 0000000000..40fe3e29eb --- /dev/null +++ b/libs/hra-ui/design-system/eslint.config.mjs @@ -0,0 +1,3 @@ +import { configs } from '../../../eslint.config.mjs'; + +export default [...configs.base, ...configs.lib, ...configs.angular]; diff --git a/libs/hra-ui/design-system/jest.config.ts b/libs/hra-ui/design-system/jest.config.ts new file mode 100644 index 0000000000..277e72c44f --- /dev/null +++ b/libs/hra-ui/design-system/jest.config.ts @@ -0,0 +1,6 @@ +export default { + displayName: 'design-system', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/hra-ui/design-system', +}; diff --git a/libs/hra-ui/design-system/ng-package.json b/libs/hra-ui/design-system/ng-package.json new file mode 100644 index 0000000000..5331a6ed0e --- /dev/null +++ b/libs/hra-ui/design-system/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/hra-ui/design-system", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/hra-ui/design-system/package.json b/libs/hra-ui/design-system/package.json new file mode 100644 index 0000000000..3375b5017f --- /dev/null +++ b/libs/hra-ui/design-system/package.json @@ -0,0 +1,6 @@ +{ + "name": "@hra-ui/design-system-v2", + "version": "0.0.1", + "peerDependencies": {}, + "sideEffects": false +} diff --git a/libs/hra-ui/design-system/project.json b/libs/hra-ui/design-system/project.json new file mode 100644 index 0000000000..195c863306 --- /dev/null +++ b/libs/hra-ui/design-system/project.json @@ -0,0 +1,19 @@ +{ + "name": "design-system-v2", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/hra-ui/design-system/src", + "prefix": "hra", + "projectType": "library", + "tags": ["type:lib", "project:design-system"], + "targets": { + "build": { + "executor": "@nx/angular:package" + }, + "compodoc": { + "executor": "@twittwer/compodoc:compodoc", + "options": { + "tsConfig": "libs/hra-ui/design-system/tsconfig.lib.json" + } + } + } +} diff --git a/libs/hra-ui/design-system/src/index.ts b/libs/hra-ui/design-system/src/index.ts new file mode 100644 index 0000000000..13e17abbbf --- /dev/null +++ b/libs/hra-ui/design-system/src/index.ts @@ -0,0 +1,2 @@ +/** A placeholder value until code is moved to this library */ +export const placeholder = 'placeholder'; diff --git a/libs/hra-ui/design-system/src/lib/.gitkeep b/libs/hra-ui/design-system/src/lib/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/hra-ui/design-system/src/test-setup.ts b/libs/hra-ui/design-system/src/test-setup.ts new file mode 100644 index 0000000000..1928dfb03e --- /dev/null +++ b/libs/hra-ui/design-system/src/test-setup.ts @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom'; +import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; + +setupZonelessTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/hra-ui/design-system/tsconfig.json b/libs/hra-ui/design-system/tsconfig.json new file mode 100644 index 0000000000..bef7691262 --- /dev/null +++ b/libs/hra-ui/design-system/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": {}, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/hra-ui/design-system/tsconfig.lib.json b/libs/hra-ui/design-system/tsconfig.lib.json new file mode 100644 index 0000000000..1932a18fb7 --- /dev/null +++ b/libs/hra-ui/design-system/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "**/*.test.ts"] +} diff --git a/libs/hra-ui/design-system/tsconfig.lib.prod.json b/libs/hra-ui/design-system/tsconfig.lib.prod.json new file mode 100644 index 0000000000..2a2faa884c --- /dev/null +++ b/libs/hra-ui/design-system/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/libs/hra-ui/design-system/tsconfig.spec.json b/libs/hra-ui/design-system/tsconfig.spec.json new file mode 100644 index 0000000000..2b98a4e8c5 --- /dev/null +++ b/libs/hra-ui/design-system/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["jest", "node", "jest-dom"], + "isolatedModules": true + }, + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/hra-ui/router-ext/README.md b/libs/hra-ui/router-ext/README.md new file mode 100644 index 0000000000..2bb4a3145b --- /dev/null +++ b/libs/hra-ui/router-ext/README.md @@ -0,0 +1,7 @@ +# router-ext + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test router-ext` to execute the unit tests. diff --git a/libs/hra-ui/router-ext/eslint.config.mjs b/libs/hra-ui/router-ext/eslint.config.mjs new file mode 100644 index 0000000000..40fe3e29eb --- /dev/null +++ b/libs/hra-ui/router-ext/eslint.config.mjs @@ -0,0 +1,3 @@ +import { configs } from '../../../eslint.config.mjs'; + +export default [...configs.base, ...configs.lib, ...configs.angular]; diff --git a/libs/hra-ui/router-ext/jest.config.ts b/libs/hra-ui/router-ext/jest.config.ts new file mode 100644 index 0000000000..8df305a09c --- /dev/null +++ b/libs/hra-ui/router-ext/jest.config.ts @@ -0,0 +1,6 @@ +export default { + displayName: 'router-ext', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/hra-ui/router-ext', +}; diff --git a/libs/hra-ui/router-ext/ng-package.json b/libs/hra-ui/router-ext/ng-package.json new file mode 100644 index 0000000000..e76c8b9548 --- /dev/null +++ b/libs/hra-ui/router-ext/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/hra-ui/router-ext", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/hra-ui/router-ext/package.json b/libs/hra-ui/router-ext/package.json new file mode 100644 index 0000000000..b9aa309cad --- /dev/null +++ b/libs/hra-ui/router-ext/package.json @@ -0,0 +1,6 @@ +{ + "name": "@hra-ui/router-ext", + "version": "0.0.1", + "peerDependencies": {}, + "sideEffects": false +} diff --git a/libs/hra-ui/router-ext/project.json b/libs/hra-ui/router-ext/project.json new file mode 100644 index 0000000000..9121a23517 --- /dev/null +++ b/libs/hra-ui/router-ext/project.json @@ -0,0 +1,19 @@ +{ + "name": "router-ext", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/hra-ui/router-ext/src", + "prefix": "hra", + "projectType": "library", + "tags": ["type:lib", "project:router-ext"], + "targets": { + "build": { + "executor": "@nx/angular:package" + }, + "compodoc": { + "executor": "@twittwer/compodoc:compodoc", + "options": { + "tsConfig": "libs/hra-ui/router-ext/tsconfig.lib.json" + } + } + } +} diff --git a/libs/hra-ui/router-ext/src/index.ts b/libs/hra-ui/router-ext/src/index.ts new file mode 100644 index 0000000000..13e17abbbf --- /dev/null +++ b/libs/hra-ui/router-ext/src/index.ts @@ -0,0 +1,2 @@ +/** A placeholder value until code is moved to this library */ +export const placeholder = 'placeholder'; diff --git a/libs/hra-ui/router-ext/src/lib/.gitkeep b/libs/hra-ui/router-ext/src/lib/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/hra-ui/router-ext/src/test-setup.ts b/libs/hra-ui/router-ext/src/test-setup.ts new file mode 100644 index 0000000000..1928dfb03e --- /dev/null +++ b/libs/hra-ui/router-ext/src/test-setup.ts @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom'; +import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; + +setupZonelessTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/hra-ui/router-ext/tsconfig.json b/libs/hra-ui/router-ext/tsconfig.json new file mode 100644 index 0000000000..bef7691262 --- /dev/null +++ b/libs/hra-ui/router-ext/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": {}, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/hra-ui/router-ext/tsconfig.lib.json b/libs/hra-ui/router-ext/tsconfig.lib.json new file mode 100644 index 0000000000..1932a18fb7 --- /dev/null +++ b/libs/hra-ui/router-ext/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "**/*.test.ts"] +} diff --git a/libs/hra-ui/router-ext/tsconfig.lib.prod.json b/libs/hra-ui/router-ext/tsconfig.lib.prod.json new file mode 100644 index 0000000000..2a2faa884c --- /dev/null +++ b/libs/hra-ui/router-ext/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/libs/hra-ui/router-ext/tsconfig.spec.json b/libs/hra-ui/router-ext/tsconfig.spec.json new file mode 100644 index 0000000000..2b98a4e8c5 --- /dev/null +++ b/libs/hra-ui/router-ext/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["jest", "node", "jest-dom"], + "isolatedModules": true + }, + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/hra-ui/utils/README.md b/libs/hra-ui/utils/README.md new file mode 100644 index 0000000000..7c44079c5f --- /dev/null +++ b/libs/hra-ui/utils/README.md @@ -0,0 +1,7 @@ +# utils + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test utils` to execute the unit tests. diff --git a/libs/hra-ui/utils/di/ng-package.json b/libs/hra-ui/utils/di/ng-package.json new file mode 100644 index 0000000000..c781f0df46 --- /dev/null +++ b/libs/hra-ui/utils/di/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/hra-ui/utils/di/src/index.ts b/libs/hra-ui/utils/di/src/index.ts new file mode 100644 index 0000000000..47616e9bf1 --- /dev/null +++ b/libs/hra-ui/utils/di/src/index.ts @@ -0,0 +1,2 @@ +export { createProviderFeature, getProvidersForFeatures, ProviderFeature } from './lib/provider-feature'; +export { InjectOptionsExt } from './lib/types'; diff --git a/libs/hra-ui/utils/di/src/lib/provider-feature.spec.ts b/libs/hra-ui/utils/di/src/lib/provider-feature.spec.ts new file mode 100644 index 0000000000..df1d905fa9 --- /dev/null +++ b/libs/hra-ui/utils/di/src/lib/provider-feature.spec.ts @@ -0,0 +1,33 @@ +import { Provider } from '@angular/core'; +import { createProviderFeature, getProvidersForFeatures } from './provider-feature'; + +describe('ProviderFeature', () => { + enum TestFeatureKind { + FeatureA = 'FeatureA', + FeatureB = 'FeatureB', + } + + const providers: Provider[] = [ + { provide: 'TestProvider', useValue: 'TestValue' }, + { provide: 'AnotherProvider', useValue: 'AnotherValue' }, + ]; + + describe('createProviderFeature', () => { + it('should create a provider feature with the correct kind and providers', () => { + const feature = createProviderFeature(TestFeatureKind.FeatureA, providers); + expect(feature).toEqual({ + kind: TestFeatureKind.FeatureA, + providers: providers, + }); + }); + }); + + describe('getProvidersForFeatures', () => { + it('should get all providers from a set of features', () => { + const featureA = createProviderFeature(TestFeatureKind.FeatureA, [providers[0]]); + const featureB = createProviderFeature(TestFeatureKind.FeatureB, [providers[1]]); + const result = getProvidersForFeatures([featureA, featureB]); + expect(result).toEqual(providers); + }); + }); +}); diff --git a/libs/hra-ui/utils/di/src/lib/provider-feature.ts b/libs/hra-ui/utils/di/src/lib/provider-feature.ts new file mode 100644 index 0000000000..255a2627f2 --- /dev/null +++ b/libs/hra-ui/utils/di/src/lib/provider-feature.ts @@ -0,0 +1,71 @@ +import { EnvironmentProviders, Provider } from '@angular/core'; +import { Tagged } from 'type-fest'; + +/** + * Provider feature for simplifying the implementation of the + * [provider feature pattern](https://angular.dev/guide/di/defining-dependency-providers#advanced-provider-patterns-with-options) in Angular. + * + * Basic example of how to create and use provider features: + * ```ts + * // Define a feature kind enum + * enum MyFeatureKind { + * FeatureA, + * FeatureB, + * } + * + * // Create a feature type + * type MyFeature = ProviderFeature; + * + * // Feature factory functions + * function withMyFeatureA(): MyFeature { + * return createProviderFeature(MyFeatureKind.FeatureA, [FeatureAProvider]); + * } + * + * function withMyFeatureB(options: FeatureBOptions): MyFeature { + * return createProviderFeature(MyFeatureKind.FeatureB, [{ provide: FeatureBConfig, useValue: options }]); + * } + * + * // Provider function + * function provideMyFeature(...features: MyFeature[]): EnvironmentProvider { + * return makeEnvironmentProviders([ + * // Required providers for this feature, etc. + * MyRequiredProvider, + * // Add individual feature providers + * ...getProvidersForFeatures(features) + * ]); + * } + * ``` + */ +export type ProviderFeature = Tagged< + { + readonly kind: KindT; + readonly providers: readonly ProviderT[]; + }, + 'ProviderFeature' +>; + +/** + * Create a provider feature object with a kind and providers + * + * @param kind Feature kind + * @param providers Array of providers for the feature + * @returns A provider feature object + */ +export function createProviderFeature( + kind: KindT, + providers: readonly ProviderT[], +): ProviderFeature { + return { kind, providers } as ProviderFeature; +} + +/** + * Get all providers from an array of provider features as a flat array + * + * @param features Array of provider features + * @returns Flat array of all providers from the features + */ +export function getProvidersForFeatures( + features: readonly ProviderFeature[], +): ProviderT[] { + return features.flatMap((feature) => feature.providers); +} diff --git a/libs/hra-ui/utils/di/src/lib/types.ts b/libs/hra-ui/utils/di/src/lib/types.ts new file mode 100644 index 0000000000..403a056c61 --- /dev/null +++ b/libs/hra-ui/utils/di/src/lib/types.ts @@ -0,0 +1,18 @@ +import { InjectOptions, Injector } from '@angular/core'; + +/** + * Extended `InjectOptions` for inject style functions. + * Takes a `Strict` type parameter to simplify typing overloads for optional vs non-optional injects. + * + * @example + * ```ts + * function injectMyService(options: InjectOptionsExt): MyService; + * function injectMyService(options: InjectOptionsExt): MyService | null; + * function injectMyService(options: InjectOptionsExt): MyService | null { + * // Perform injection + * } + * ``` + */ +export type InjectOptionsExt = InjectOptions & { injector?: Injector } & { + optional?: Strict extends true ? false : boolean; +}; diff --git a/libs/hra-ui/utils/eslint.config.mjs b/libs/hra-ui/utils/eslint.config.mjs new file mode 100644 index 0000000000..40fe3e29eb --- /dev/null +++ b/libs/hra-ui/utils/eslint.config.mjs @@ -0,0 +1,3 @@ +import { configs } from '../../../eslint.config.mjs'; + +export default [...configs.base, ...configs.lib, ...configs.angular]; diff --git a/libs/hra-ui/utils/jest.config.ts b/libs/hra-ui/utils/jest.config.ts new file mode 100644 index 0000000000..fb1a460d65 --- /dev/null +++ b/libs/hra-ui/utils/jest.config.ts @@ -0,0 +1,6 @@ +export default { + displayName: 'utils', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/hra-ui/utils', +}; diff --git a/libs/hra-ui/utils/ng-package.json b/libs/hra-ui/utils/ng-package.json new file mode 100644 index 0000000000..27182bcad7 --- /dev/null +++ b/libs/hra-ui/utils/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/hra-ui/utils", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/hra-ui/utils/package.json b/libs/hra-ui/utils/package.json new file mode 100644 index 0000000000..d08e6af520 --- /dev/null +++ b/libs/hra-ui/utils/package.json @@ -0,0 +1,10 @@ +{ + "name": "@hra-ui/utils", + "version": "0.0.1", + "peerDependencies": { + "@angular/core": "^21.1.5", + "@angular/router": "^21.1.5", + "type-fest": "^5.4.4" + }, + "sideEffects": false +} diff --git a/libs/hra-ui/utils/paths/ng-package.json b/libs/hra-ui/utils/paths/ng-package.json new file mode 100644 index 0000000000..c781f0df46 --- /dev/null +++ b/libs/hra-ui/utils/paths/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/hra-ui/utils/paths/src/index.ts b/libs/hra-ui/utils/paths/src/index.ts new file mode 100644 index 0000000000..77b9649361 --- /dev/null +++ b/libs/hra-ui/utils/paths/src/index.ts @@ -0,0 +1,3 @@ +export { IsActiveMatchOptions, isUrlActive } from './lib/active-match'; +export { joinWithSlashes, stripLeadingHash, stripTrailingSlash } from './lib/path'; +export { canParseUrl, isAbsoluteUrl, isRelativeUrl, parseUrl } from './lib/url'; diff --git a/libs/hra-ui/utils/paths/src/lib/active-match.spec.ts b/libs/hra-ui/utils/paths/src/lib/active-match.spec.ts new file mode 100644 index 0000000000..53d3f112a1 --- /dev/null +++ b/libs/hra-ui/utils/paths/src/lib/active-match.spec.ts @@ -0,0 +1,95 @@ +import { IsActiveMatchOptions } from '@angular/router'; +import { isUrlActive } from './active-match'; + +describe('isUrlActive', () => { + const TARGET_EMPTY = ''; + const TARGET_SLASH_ONLY = '/'; + const TARGET_WITH_ORIGIN = 'https://example.com'; + const TARGET_WITH_ORIGIN_2 = 'https://other.example.com'; + const TARGET_WITH_PATH = '/foo/bar'; + const TARGET_WITH_MATRIX_PARAMS = '/foo;a=1;b=2/bar'; + const TARGET_WITH_QUERY = '/foo/bar?a=1'; + const TARGET_WITH_FRAGMENT = '/foo/bar#abc'; + const TARGET_WITH_EVERYTHING = '/foo;a=1;b=2/bar?a=1&b=2#abc'; + const CURRENT_EMPTY = ''; + const CURRENT_WITH_ORIGIN = 'https://example.com'; + const CURRENT_WITH_PATH = `${CURRENT_WITH_ORIGIN}/foo/bar`; + const CURRENT_WITH_PATH_2 = `${CURRENT_WITH_ORIGIN}/foo/bar/baz`; + const CURRENT_WITH_PATH_3 = `${CURRENT_WITH_ORIGIN}/abc/def`; + const CURRENT_WITH_MATRIX_PARAMS = `${CURRENT_WITH_ORIGIN}/foo;a=1;b=2/bar`; + const CURRENT_WITH_MATRIX_PARAMS_2 = `${CURRENT_WITH_ORIGIN}/foo;b=2;a=1/bar`; + const CURRENT_WITH_MATRIX_PARAMS_3 = `${CURRENT_WITH_ORIGIN}/foo;a=1;b=2;c=3/bar`; + const CURRENT_WITH_MATRIX_PARAMS_4 = `${CURRENT_WITH_ORIGIN}/foo;a=3;b=2/bar`; + const CURRENT_WITH_QUERY = `${CURRENT_WITH_ORIGIN}/foo/bar?a=1`; + const CURRENT_WITH_QUERY_2 = `${CURRENT_WITH_ORIGIN}/foo/bar?a=1&b=2`; + const CURRENT_WITH_QUERY_3 = `${CURRENT_WITH_ORIGIN}/foo/bar?b=2&a=1`; + const CURRENT_WITH_QUERY_4 = `${CURRENT_WITH_ORIGIN}/foo/bar?a=3`; + const CURRENT_WITH_FRAGMENT = `${CURRENT_WITH_ORIGIN}/foo/bar#abc`; + const CURRENT_WITH_FRAGMENT_2 = `${CURRENT_WITH_ORIGIN}/foo/bar#def`; + const CURRENT_WITH_EVERYTHING = `${CURRENT_WITH_ORIGIN}/foo;a=1;b=2/bar?a=1&b=2#abc`; + + const DEFAULT_MATCH_OPTIONS: IsActiveMatchOptions = { + fragment: 'ignored', + matrixParams: 'ignored', + paths: 'subset', + queryParams: 'ignored', + }; + + type TestCase = [targetUrl: string, currentUrl: string, options: Partial, result: boolean]; + const TEST_CASES: TestCase[] = [ + // Origin checks + [TARGET_EMPTY, CURRENT_EMPTY, {}, true], + [TARGET_EMPTY, CURRENT_WITH_ORIGIN, {}, true], + [TARGET_SLASH_ONLY, CURRENT_WITH_ORIGIN, {}, true], + [TARGET_WITH_ORIGIN, CURRENT_WITH_ORIGIN, {}, true], + [TARGET_WITH_ORIGIN, CURRENT_EMPTY, {}, false], + [TARGET_WITH_ORIGIN_2, CURRENT_WITH_ORIGIN, {}, false], + + // Path checks + [TARGET_WITH_PATH, CURRENT_WITH_PATH, {}, true], + [TARGET_WITH_PATH, CURRENT_WITH_PATH_2, {}, true], + [TARGET_WITH_PATH, CURRENT_WITH_PATH, { paths: 'exact' }, true], + [TARGET_WITH_PATH, CURRENT_WITH_PATH_2, { paths: 'exact' }, false], + [TARGET_WITH_PATH, CURRENT_WITH_PATH_3, {}, false], + + // Matrix parameter checks + [TARGET_WITH_MATRIX_PARAMS, CURRENT_WITH_PATH, {}, true], + [TARGET_WITH_MATRIX_PARAMS, CURRENT_WITH_MATRIX_PARAMS, { matrixParams: 'exact' }, true], + [TARGET_WITH_MATRIX_PARAMS, CURRENT_WITH_MATRIX_PARAMS_2, { matrixParams: 'exact' }, true], + [TARGET_WITH_MATRIX_PARAMS, CURRENT_WITH_MATRIX_PARAMS_3, { matrixParams: 'subset' }, true], + [TARGET_WITH_MATRIX_PARAMS, CURRENT_WITH_MATRIX_PARAMS_3, { matrixParams: 'exact' }, false], + [TARGET_WITH_MATRIX_PARAMS, CURRENT_WITH_MATRIX_PARAMS_4, { matrixParams: 'subset' }, false], + [TARGET_WITH_MATRIX_PARAMS, CURRENT_WITH_MATRIX_PARAMS_4, { matrixParams: 'exact' }, false], + + // Query checks + [TARGET_WITH_QUERY, CURRENT_WITH_PATH, {}, true], + [TARGET_WITH_QUERY, CURRENT_WITH_QUERY, { queryParams: 'subset' }, true], + [TARGET_WITH_QUERY, CURRENT_WITH_QUERY, { queryParams: 'exact' }, true], + [TARGET_WITH_QUERY, CURRENT_WITH_QUERY_2, { queryParams: 'subset' }, true], + [TARGET_WITH_QUERY, CURRENT_WITH_QUERY_3, { queryParams: 'subset' }, true], + [TARGET_WITH_QUERY, CURRENT_WITH_QUERY_2, { queryParams: 'exact' }, false], + [TARGET_WITH_QUERY, CURRENT_WITH_QUERY_4, { queryParams: 'subset' }, false], + [TARGET_WITH_QUERY, CURRENT_WITH_QUERY_4, { queryParams: 'exact' }, false], + + // Fragment checks + [TARGET_WITH_FRAGMENT, CURRENT_WITH_PATH, {}, true], + [TARGET_WITH_FRAGMENT, CURRENT_WITH_FRAGMENT, { fragment: 'exact' }, true], + [TARGET_WITH_FRAGMENT, CURRENT_WITH_PATH, { fragment: 'exact' }, false], + [TARGET_WITH_FRAGMENT, CURRENT_WITH_FRAGMENT_2, { fragment: 'exact' }, false], + + // Everything check + [ + TARGET_WITH_EVERYTHING, + CURRENT_WITH_EVERYTHING, + { paths: 'exact', matrixParams: 'exact', queryParams: 'exact', fragment: 'exact' }, + true, + ], + ]; + + it.each(TEST_CASES)( + "should match '%s' against '%s' with options %j with a result of %p", + (targetUrl, currentUrl, options, expected) => { + expect(isUrlActive(targetUrl, currentUrl, { ...DEFAULT_MATCH_OPTIONS, ...options })).toEqual(expected); + }, + ); +}); diff --git a/libs/hra-ui/utils/paths/src/lib/active-match.ts b/libs/hra-ui/utils/paths/src/lib/active-match.ts new file mode 100644 index 0000000000..1dfeeb4554 --- /dev/null +++ b/libs/hra-ui/utils/paths/src/lib/active-match.ts @@ -0,0 +1,205 @@ +// TODO move into router-ext? +import { IsActiveMatchOptions } from '@angular/router'; +import { stripLeadingHash } from './path'; +import { parseUrl } from './url'; + +// Reexport options interface +export { IsActiveMatchOptions }; + +/** A key-value pair */ +type Param = [key: string, value: string]; + +/** A path segment with matrix parameters */ +interface SegmentWithMatrixParams { + /** Path segment */ + segment: string; + /** Path matrix parameters */ + matrix: Param[]; +} + +/** + * Check whether a set could be a subset of another set based on their sizes. + * + * @param size1 Size of the first set + * @param size2 Size of the second set + * @param exact Whether the sizes should match exactly + * @returns true if the `size1` is less than or equal to `size2`, false otherwise + */ +function checkSubsetSize(size1: number, size2: number, exact: boolean): boolean { + return exact ? size1 === size2 : size1 <= size2; +} + +/** + * Check whether a set is a subset of another set. Keys are compared case insensitively and values are compared case sensitively. + * This function assumes that the first set is smaller than or equal to the second set. + * + * @param iterable1 The first set + * @param iterable2 The second set + * @returns true if the first set is a subset of the second set, false otherwise + */ +function checkSubset(iterable1: Iterable, iterable2: Iterable): boolean { + const set: (Param | undefined)[] = Array.from(iterable2).map(([key, value]): Param => [key.toLowerCase(), value]); + + for (const [key, value] of iterable1) { + const icaseKey = key.toLowerCase(); + const index = set.findIndex((entry) => entry && entry[0] === icaseKey && entry[1] === value); + + if (index < 0) { + return false; + } + + set[index] = undefined; + } + + return true; +} + +/** + * Parse a path segment with matrix parameters into an object containing the segment and its matrix parameters. + * + * @param segment The path segment to parse + * @returns An object containing the segment and its matrix parameters + */ +function parseSegment(segment: string): SegmentWithMatrixParams { + const parts = segment.split(';'); + const name = parts.shift() as string; + const matrix: Param[] = []; + + for (const param of parts) { + const [key, ...values] = param.split('='); + matrix.push([key, values.join('=')]); + } + + return { segment: name, matrix }; +} + +/** + * Parse a path into an array of segments with their matrix parameters. + * The path is normalized by removing leading and trailing slashes before parsing. + * + * @param path The path to parse + * @returns An array of segments with their matrix parameters + */ +function parsePathSegments(path: string): SegmentWithMatrixParams[] { + path = path.replace(/^\/+|\/+$/g, ''); + if (!path) { + return []; + } + + return path.split('/').map(parseSegment); +} + +/** + * Compare two sets of matrix parameters based on the provided options. + * + * @param params1 The first set of matrix parameters + * @param params2 The second set of matrix parameters + * @param options The options to determine how the comparison should be performed + * @returns true if the sets of matrix parameters match based on the options, false otherwise + */ +function compareMatrixParams(params1: Param[], params2: Param[], options: IsActiveMatchOptions): boolean { + if (options.matrixParams === 'ignored') { + return true; + } else if (!checkSubsetSize(params1.length, params2.length, options.matrixParams === 'exact')) { + return false; + } + + return checkSubset(params1, params2); +} + +/** + * Compare two paths that may contain matrix parameters based on the provided options. + * + * @param path1 The first path to compare + * @param path2 The second path to compare + * @param options The options to determine how the comparison should be performed + * @returns true if the paths match based on the options, false otherwise + */ +function comparePathsWithMatrixParams(path1: string, path2: string, options: IsActiveMatchOptions): boolean { + if (path1 === path2) { + return true; + } + + const parsed1 = parsePathSegments(path1); + const parsed2 = parsePathSegments(path2); + if (!checkSubsetSize(parsed1.length, parsed2.length, options.paths === 'exact')) { + return false; + } + + for (let index = 0; index < parsed1.length; index++) { + const segment1 = parsed1[index]; + const segment2 = parsed2[index]; + if (segment1.segment !== segment2.segment || !compareMatrixParams(segment1.matrix, segment2.matrix, options)) { + return false; + } + } + + return true; +} + +/** + * Compare two sets of query parameters based on the provided options. + * + * @param params1 The first set of query parameters + * @param params2 The second set of query parameters + * @param options The options to determine how the comparison should be performed + * @returns true if the sets of query parameters match based on the options, false otherwise + */ +function compareQueryParams( + params1: URLSearchParams, + params2: URLSearchParams, + options: IsActiveMatchOptions, +): boolean { + if (options.queryParams === 'ignored') { + return true; + } else if (!checkSubsetSize(params1.size, params2.size, options.queryParams === 'exact')) { + return false; + } + + return checkSubset(params1, params2); +} + +/** + * Compare two fragments based on the provided options. + * + * @param fragment1 The first fragment to compare + * @param fragment2 The second fragment to compare + * @param options The options to determine how the comparison should be performed + * @returns true if the fragments match based on the options, false otherwise + */ +function compareFragments(fragment1: string, fragment2: string, options: IsActiveMatchOptions): boolean { + return options.fragment === 'ignored' || stripLeadingHash(fragment1) === stripLeadingHash(fragment2); +} + +/** + * Compare two urls similar to how `Router.isActive` compares url trees. + * The urls are parsed and compared based on the provided options. + * + * @param targetUrl The target url to compare + * @param currentUrl The current url to compare + * @param options The options to determine how the comparison should be performed + * @returns true if the urls match based on the options, false otherwise + */ +export function isUrlActive(targetUrl: string, currentUrl: string, options?: Partial): boolean { + const optionsWithDefaults: IsActiveMatchOptions = { + paths: 'subset', + queryParams: 'subset', + matrixParams: 'ignored', + fragment: 'ignored', + ...options, + }; + const base = 'http://example.com'; + const url1 = parseUrl(targetUrl, base); + const url2 = parseUrl(currentUrl, base); + if (!url1 || !url2) { + return false; + } + + const sameOrigin = url1.origin === url2.origin || url1.origin === base; + return ( + sameOrigin && + comparePathsWithMatrixParams(url1.pathname, url2.pathname, optionsWithDefaults) && + compareQueryParams(url1.searchParams, url2.searchParams, optionsWithDefaults) && + compareFragments(url1.hash, url2.hash, optionsWithDefaults) + ); +} diff --git a/libs/hra-ui/utils/paths/src/lib/path.spec.ts b/libs/hra-ui/utils/paths/src/lib/path.spec.ts new file mode 100644 index 0000000000..1afc07a69f --- /dev/null +++ b/libs/hra-ui/utils/paths/src/lib/path.spec.ts @@ -0,0 +1,52 @@ +import { joinWithSlashes, stripLeadingHash, stripTrailingSlash } from './path'; + +describe('Path utilities', () => { + describe('joinWithSlashes', () => { + it('should join multiple path segments with slashes', () => { + expect(joinWithSlashes('a', 'b', 'c')).toEqual('a/b/c'); + expect(joinWithSlashes('/a/', '/b/', '/c/')).toEqual('/a/b/c/'); + expect(joinWithSlashes('a/', '/b', 'c/')).toEqual('a/b/c/'); + }); + + it('should preserve leading and trailing slashes', () => { + expect(joinWithSlashes('/a', 'b', 'c/')).toEqual('/a/b/c/'); + expect(joinWithSlashes('/a/', 'b', 'c')).toEqual('/a/b/c'); + }); + + it('should skip empty segments', () => { + expect(joinWithSlashes('', 'a', '', 'b', '', 'c', '')).toEqual('a/b/c'); + }); + + it('should not resolve relative path segments', () => { + expect(joinWithSlashes('a', '.', 'b', '..', 'c')).toEqual('a/./b/../c'); + }); + }); + + describe('stripTrailingSlash', () => { + it('should remove the trailing slash from a path', () => { + expect(stripTrailingSlash('/path/')).toEqual('/path'); + expect(stripTrailingSlash('/path/to/resource/')).toEqual('/path/to/resource'); + }); + + it('should preserve the fragment and query parameters', () => { + expect(stripTrailingSlash('/path/?query=string#fragment')).toEqual('/path?query=string#fragment'); + expect(stripTrailingSlash('/path/#fragment')).toEqual('/path#fragment'); + }); + + it('should not modify paths without a trailing slash', () => { + expect(stripTrailingSlash('/path')).toEqual('/path'); + expect(stripTrailingSlash('/path?query=string#fragment')).toEqual('/path?query=string#fragment'); + }); + }); + + describe('stripLeadingHash', () => { + it('should remove the leading hash from a fragment', () => { + expect(stripLeadingHash('#fragment')).toEqual('fragment'); + }); + + it('should not modify fragments without a leading hash', () => { + expect(stripLeadingHash('')).toEqual(''); + expect(stripLeadingHash('fragment')).toEqual('fragment'); + }); + }); +}); diff --git a/libs/hra-ui/utils/paths/src/lib/path.ts b/libs/hra-ui/utils/paths/src/lib/path.ts new file mode 100644 index 0000000000..2d23b84cf0 --- /dev/null +++ b/libs/hra-ui/utils/paths/src/lib/path.ts @@ -0,0 +1,63 @@ +/** + * Join multiple path segments into a single path with slashes, preserving leading and trailing slashes. + * Does not resolve relative path segments (e.g. "." or ".."). + * + * @param segments Path segments to join + * @returns The concatenated path + */ +export function joinWithSlashes(...segments: string[]): string { + const parts: string[] = []; + let previousEndsWithSlash = false; + for (const segment of segments) { + if (!segment) { + continue; + } + + const hasLeadingSlash = segment.startsWith('/'); + const hasTrailingSlash = segment.endsWith('/'); + if (previousEndsWithSlash && hasLeadingSlash) { + parts.push(segment.slice(1)); + } else if (!previousEndsWithSlash && !hasLeadingSlash) { + parts.push('/', segment); + } else { + parts.push(segment); + } + + previousEndsWithSlash = hasTrailingSlash; + } + + if (parts.length > 1 && parts[0] === '/') { + parts.shift(); + } + + return parts.join(''); +} + +/** + * Strip the trailing slash from a path while preserving the fragment and query parameters (if present). + * + * @param path Path to strip + * @returns New path + */ +export function stripTrailingSlash(path: string): string { + const index = path.search(/#|\?|$/); + if (index > 0 && path[index - 1] === '/') { + return path.slice(0, index - 1) + path.slice(index); + } + + return path; +} + +/** + * Strip the leading hash symbol from a fragment (if present). + * + * @param fragment Fragment to strip + * @returns New fragment + */ +export function stripLeadingHash(fragment: string): string { + if (fragment.length > 0 && fragment[0] === '#') { + return fragment.slice(1); + } + + return fragment; +} diff --git a/libs/hra-ui/utils/paths/src/lib/url.spec.ts b/libs/hra-ui/utils/paths/src/lib/url.spec.ts new file mode 100644 index 0000000000..31f4955505 --- /dev/null +++ b/libs/hra-ui/utils/paths/src/lib/url.spec.ts @@ -0,0 +1,106 @@ +import { canParseUrl, isAbsoluteUrl, isRelativeUrl, parseUrl } from './url'; + +describe('URL utilties', () => { + const HTTPS_URL = 'https://www.example.com'; + const HTTP_URL = 'http://example.com'; + const FTP_URL = 'ftp://example.com'; + const URL_WITH_PATH_QUERY_AND_HASH = 'https://example.com/path?query=string#fragment'; + + const RELATIVE_PATH = '/path/to/resource'; + const RELATIVE_NO_LEADING_SLASH_PATH = 'path/to/resource'; + const RELATIVE_PARENT_PATH = '../path/to/resource'; + const RELATIVE_CURRENT_PATH = './path/to/resource'; + + const INVALID_URL = 'not a url'; + const INVALID_SCHEME_URL = '://example.com'; + const EMPTY_URL = ''; + + const BASE_URL = HTTPS_URL; + + describe('canParseUrl', () => { + it('should return true for valid absolute URLs', () => { + expect(canParseUrl(HTTPS_URL)).toBe(true); + expect(canParseUrl(HTTP_URL)).toBe(true); + expect(canParseUrl(FTP_URL)).toBe(true); + expect(canParseUrl(URL_WITH_PATH_QUERY_AND_HASH)).toBe(true); + }); + + it('should return true for valid relative URLs if base is provided', () => { + expect(canParseUrl(RELATIVE_PATH, BASE_URL)).toBe(true); + expect(canParseUrl(RELATIVE_NO_LEADING_SLASH_PATH, BASE_URL)).toBe(true); + expect(canParseUrl(RELATIVE_PARENT_PATH, BASE_URL)).toBe(true); + expect(canParseUrl(RELATIVE_CURRENT_PATH, BASE_URL)).toBe(true); + }); + + it('should return false for invalid URLs and relative URLs without a base', () => { + expect(canParseUrl(INVALID_URL)).toBe(false); + expect(canParseUrl(INVALID_SCHEME_URL)).toBe(false); + expect(canParseUrl(EMPTY_URL)).toBe(false); + expect(canParseUrl(RELATIVE_PATH)).toBe(false); + expect(canParseUrl(RELATIVE_NO_LEADING_SLASH_PATH)).toBe(false); + expect(canParseUrl(RELATIVE_PARENT_PATH)).toBe(false); + expect(canParseUrl(RELATIVE_CURRENT_PATH)).toBe(false); + }); + }); + + describe('parseUrl', () => { + it('should correctly parse absolute URLs', () => { + const url = 'https://www.example.com/path?query=string#fragment'; + const parsed = parseUrl(url); + expect(parsed?.protocol).toBe('https:'); + expect(parsed?.hostname).toBe('www.example.com'); + expect(parsed?.pathname).toBe('/path'); + expect(parsed?.search).toBe('?query=string'); + expect(parsed?.hash).toBe('#fragment'); + }); + + it('should correctly parse relative URLs with a base', () => { + const url = '/path/to/resource?query=string#fragment'; + const parsed = parseUrl(url, BASE_URL); + expect(parsed?.protocol).toBe('https:'); + expect(parsed?.hostname).toBe('www.example.com'); + expect(parsed?.pathname).toBe('/path/to/resource'); + expect(parsed?.search).toBe('?query=string'); + expect(parsed?.hash).toBe('#fragment'); + }); + + it('should return null for invalid URLs and relative URLs without a base', () => { + expect(parseUrl(INVALID_URL)).toBeNull(); + expect(parseUrl(INVALID_SCHEME_URL)).toBeNull(); + expect(parseUrl(EMPTY_URL)).toBeNull(); + expect(parseUrl(RELATIVE_PATH)).toBeNull(); + }); + }); + + describe('isAbsoluteUrl', () => { + it('should return true for valid absolute URLs', () => { + expect(isAbsoluteUrl(HTTPS_URL)).toBe(true); + expect(isAbsoluteUrl(HTTP_URL)).toBe(true); + expect(isAbsoluteUrl(FTP_URL)).toBe(true); + expect(isAbsoluteUrl(URL_WITH_PATH_QUERY_AND_HASH)).toBe(true); + }); + + it('should return false for relative URLs', () => { + expect(isAbsoluteUrl(RELATIVE_PATH)).toBe(false); + expect(isAbsoluteUrl(RELATIVE_NO_LEADING_SLASH_PATH)).toBe(false); + expect(isAbsoluteUrl(RELATIVE_PARENT_PATH)).toBe(false); + expect(isAbsoluteUrl(RELATIVE_CURRENT_PATH)).toBe(false); + }); + }); + + describe('isRelativeUrl', () => { + it('should return true for relative URLs', () => { + expect(isRelativeUrl(RELATIVE_PATH)).toBe(true); + expect(isRelativeUrl(RELATIVE_NO_LEADING_SLASH_PATH)).toBe(true); + expect(isRelativeUrl(RELATIVE_PARENT_PATH)).toBe(true); + expect(isRelativeUrl(RELATIVE_CURRENT_PATH)).toBe(true); + }); + + it('should return false for absolute URLs', () => { + expect(isRelativeUrl(HTTPS_URL)).toBe(false); + expect(isRelativeUrl(HTTP_URL)).toBe(false); + expect(isRelativeUrl(FTP_URL)).toBe(false); + expect(isRelativeUrl(URL_WITH_PATH_QUERY_AND_HASH)).toBe(false); + }); + }); +}); diff --git a/libs/hra-ui/utils/paths/src/lib/url.ts b/libs/hra-ui/utils/paths/src/lib/url.ts new file mode 100644 index 0000000000..475724ebcd --- /dev/null +++ b/libs/hra-ui/utils/paths/src/lib/url.ts @@ -0,0 +1,62 @@ +/** + * Polyfill for `URL.canParse` for older devices and browsers. + */ +const canParseUrlPolyfill: typeof URL.canParse = (url, base) => { + try { + new URL(url, base); + return true; + } catch { + return false; + } +}; + +/** + * Test whether a URL string can be parsed as a valid URL, using the built-in `URL.canParse` if available, or a polyfill otherwise. + * + * @param url URL string to test + * @param base Optional base URL to resolve against + * @returns true if the URL can be parsed, false otherwise + */ +export const canParseUrl = 'canParse' in URL && typeof URL.canParse === 'function' ? URL.canParse : canParseUrlPolyfill; + +/** + * Polyfill for `URL.parse` for older devices and browsers. + */ +const parseUrlPolyfill: typeof URL.parse = (url, base) => { + try { + return new URL(url, base); + } catch { + return null; + } +}; + +/** + * Parse a URL string into a URL object, using the built-in `URL.parse` if available, or a polyfill otherwise. + * + * @param url URL string to parse + * @param base Optional base URL to resolve against + * @returns A URL object if parsing was successful, or null if the URL was invalid + */ +export const parseUrl = 'parse' in URL && typeof URL.parse === 'function' ? URL.parse : parseUrlPolyfill; + +/** + * Test whether a URL string is an absolute URL. + * + * @param url URL string to test + * @returns true if the URL is an absolute URL, false otherwise + */ +export function isAbsoluteUrl(url: string | URL): boolean { + return canParseUrl(url); +} + +/** + * Test whether a URL string is a relative URL (i.e. not an absolute URL). + * + * @param url URL string to test + * @returns true if the URL is a relative URL, false otherwise + */ +export function isRelativeUrl(url: string | URL): boolean { + const base = 'https://relative.url'; + const parsed = parseUrl(url, base); + return parsed !== null && parsed.origin === base; +} diff --git a/libs/shared/utils/project.json b/libs/hra-ui/utils/project.json similarity index 69% rename from libs/shared/utils/project.json rename to libs/hra-ui/utils/project.json index 76ab1376f5..831f53ecb0 100644 --- a/libs/shared/utils/project.json +++ b/libs/hra-ui/utils/project.json @@ -1,9 +1,9 @@ { - "name": "shared-utils", + "name": "utils", "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/hra-ui/utils/src", + "prefix": "hra", "projectType": "library", - "sourceRoot": "libs/shared/utils/src", - "prefix": "ftu", "tags": ["type:lib", "project:utils"], "targets": { "build": { @@ -12,7 +12,7 @@ "compodoc": { "executor": "@twittwer/compodoc:compodoc", "options": { - "tsConfig": "libs/shared/utils/tsconfig.lib.json" + "tsConfig": "libs/hra-ui/utils/tsconfig.lib.json" } } } diff --git a/libs/hra-ui/utils/src/index.ts b/libs/hra-ui/utils/src/index.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/libs/hra-ui/utils/src/index.ts @@ -0,0 +1 @@ +export default {}; diff --git a/libs/hra-ui/utils/src/lib/.gitkeep b/libs/hra-ui/utils/src/lib/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/hra-ui/utils/src/test-setup.ts b/libs/hra-ui/utils/src/test-setup.ts new file mode 100644 index 0000000000..1928dfb03e --- /dev/null +++ b/libs/hra-ui/utils/src/test-setup.ts @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom'; +import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; + +setupZonelessTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/hra-ui/utils/tsconfig.json b/libs/hra-ui/utils/tsconfig.json new file mode 100644 index 0000000000..bef7691262 --- /dev/null +++ b/libs/hra-ui/utils/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": {}, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/shared/utils/tsconfig.lib.json b/libs/hra-ui/utils/tsconfig.lib.json similarity index 65% rename from libs/shared/utils/tsconfig.lib.json rename to libs/hra-ui/utils/tsconfig.lib.json index 5657fbf456..3a411cbda8 100644 --- a/libs/shared/utils/tsconfig.lib.json +++ b/libs/hra-ui/utils/tsconfig.lib.json @@ -7,6 +7,6 @@ "inlineSources": true, "types": [] }, - "exclude": ["test-setup.ts", "**/*.spec.ts", "jest.config.ts", "**/*.test.ts"], - "include": ["**/*.ts"] + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts", "test-setup.ts", "jest.config.ts", "**/*.test.ts"] } diff --git a/libs/hra-ui/utils/tsconfig.lib.prod.json b/libs/hra-ui/utils/tsconfig.lib.prod.json new file mode 100644 index 0000000000..2a2faa884c --- /dev/null +++ b/libs/hra-ui/utils/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/libs/hra-ui/utils/tsconfig.spec.json b/libs/hra-ui/utils/tsconfig.spec.json new file mode 100644 index 0000000000..2b98a4e8c5 --- /dev/null +++ b/libs/hra-ui/utils/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["jest", "node", "jest-dom"], + "isolatedModules": true + }, + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/shared/utils/package.json b/libs/shared/utils/package.json deleted file mode 100644 index 8290ff91dd..0000000000 --- a/libs/shared/utils/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "@hra-ui/utils", - "version": "1.0.0", - "peerDependencies": { - "@angular/common": "^21.1.4", - "@angular/core": "^21.1.4", - "@angular/platform-browser": "^21.1.4", - "@angular/router": "^21.1.4" - }, - "dependencies": {}, - "sideEffects": false -} diff --git a/libs/shared/utils/src/index.ts b/libs/shared/utils/src/index.ts deleted file mode 100644 index 36893470cd..0000000000 --- a/libs/shared/utils/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/url/create'; diff --git a/libs/shared/utils/src/test-setup.ts b/libs/shared/utils/src/test-setup.ts deleted file mode 100644 index ea414013fc..0000000000 --- a/libs/shared/utils/src/test-setup.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; - -setupZoneTestEnv({ - errorOnUnknownElements: true, - errorOnUnknownProperties: true, -}); diff --git a/libs/shared/utils/tsconfig.json b/libs/shared/utils/tsconfig.json deleted file mode 100644 index 550c3e026a..0000000000 --- a/libs/shared/utils/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "useDefineForClassFields": false - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ], - "extends": "../../../tsconfig.base.json", - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true - } -} diff --git a/libs/shared/utils/types/README.md b/libs/shared/utils/types/README.md deleted file mode 100644 index 922781ca57..0000000000 --- a/libs/shared/utils/types/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @hra-ui/utils/types - -Secondary entry point of `@hra-ui/utils`. It can be used by importing from `@hra-ui/utils/types`. diff --git a/libs/shared/utils/types/src/index.ts b/libs/shared/utils/types/src/index.ts deleted file mode 100644 index 7eaf010f4e..0000000000 --- a/libs/shared/utils/types/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { Any } from './lib/any'; -export type { AnyAbstractConstructor, AnyConstructor, AnyFunction, SignatureOf } from './lib/function'; -export type { UnionMember } from './lib/union'; diff --git a/libs/shared/utils/types/src/lib/any.ts b/libs/shared/utils/types/src/lib/any.ts deleted file mode 100644 index 490c8d7fa2..0000000000 --- a/libs/shared/utils/types/src/lib/any.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * `any` alias. DO NOT use to type function/method parameters and return value. - * This alias is intended to only be used in generic types in the rare cases - * where `unknown` does not work. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type Any = any; diff --git a/libs/shared/utils/types/src/lib/function.ts b/libs/shared/utils/types/src/lib/function.ts deleted file mode 100644 index 2731fa86e0..0000000000 --- a/libs/shared/utils/types/src/lib/function.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Any } from './any'; - -/** - * Any function type. - * Primarly for use as constraint on generic types. - */ -export type AnyFunction = (...args: Any[]) => Any; - -/** - * Any constructor type. - * Primarly for use as constraint on generic types. - */ -export type AnyConstructor = new (...args: Any[]) => Any; - -/** - * Any constructor type. - * Primarly for use as constraint on generic types. - */ -export type AnyAbstractConstructor = abstract new (...args: Any[]) => Any; - -/** - * Extracts the pure function signature from any function with properties. - */ -export type SignatureOf = Fn extends (...args: infer Args) => infer Res - ? (...args: Args) => Res - : never; diff --git a/libs/shared/utils/types/src/lib/union.ts b/libs/shared/utils/types/src/lib/union.ts deleted file mode 100644 index 0bf23f6cac..0000000000 --- a/libs/shared/utils/types/src/lib/union.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Extract a member of a discriminated union. - */ -export type UnionMember = Extract; diff --git a/nx.json b/nx.json index c95003e190..a7bf8e7bc5 100644 --- a/nx.json +++ b/nx.json @@ -1,19 +1,13 @@ { - "namedInputs": { - "default": ["{projectRoot}/**/*", "sharedGlobals"], - "production": [ - "default", - "!{projectRoot}/eslint.config.js", - "!{projectRoot}/.eslintrc.json", - "!{projectRoot}/tsconfig.spec.json", - "!{projectRoot}/jest.config.[jt]s", - "!{projectRoot}/**/test-setup.[jt]s", - "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", - "!{projectRoot}/tsconfig.storybook.json", - "!{projectRoot}/.storybook/**/*", - "!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)" - ], - "sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"] + "release": { + "projects": ["libs/hra-ui/*"], + "version": { + "manifestRootsToUpdate": ["{projectRoot}", "dist/{projectRoot}"], + "currentVersionResolver": "git-tag", + "fallbackCurrentVersionResolver": "disk", + "preVersionCommand": "npx nx run-many -t build" + }, + "versionPlans": true }, "plugins": [ { @@ -33,6 +27,22 @@ "options": {} } ], + "namedInputs": { + "default": ["{projectRoot}/**/*", "sharedGlobals"], + "production": [ + "default", + "!{projectRoot}/eslint.config.js", + "!{projectRoot}/.eslintrc.json", + "!{projectRoot}/tsconfig.spec.json", + "!{projectRoot}/jest.config.[jt]s", + "!{projectRoot}/**/test-setup.[jt]s", + "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", + "!{projectRoot}/tsconfig.storybook.json", + "!{projectRoot}/.storybook/**/*", + "!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)" + ], + "sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"] + }, "targetDefaults": { "@nx/angular:application": { "cache": true, @@ -224,6 +234,11 @@ "cache": true, "dependsOn": ["^build"], "inputs": ["production", "^production"] + }, + "@nx/js:release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } } }, "generators": { diff --git a/package-lock.json b/package-lock.json index fff34f3987..d35ce1070f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -133,7 +133,7 @@ "@storybook/test-runner": "^0.24.2", "@swc-node/register": "^1.11.1", "@swc/cli": "^0.8.0", - "@swc/core": "^1.11.1", + "@swc/core": "~1.15.5", "@swc/helpers": "^0.5.18", "@testing-library/angular": "^19.1.0", "@testing-library/dom": "^10.4.1", @@ -182,6 +182,7 @@ "ts-node": "^10.9.2", "typescript": "5.9.3", "typescript-eslint": "^8.56.0", + "verdaccio": "^6.0.5", "webpack": "5.105.2" } }, @@ -5144,6 +5145,62 @@ "node": ">=18" } }, + "node_modules/@cypress/request": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", + "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.4", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "~6.14.1", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request/node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@cypress/request/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@danmarshall/deckgl-typings": { "version": "4.9.28", "resolved": "https://registry.npmjs.org/@danmarshall/deckgl-typings/-/deckgl-typings-4.9.28.tgz", @@ -14572,6 +14629,13 @@ "typescript": "^3 || ^4 || ^5" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -20574,6 +20638,16 @@ "@types/node": "*" } }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", @@ -21276,6 +21350,609 @@ "win32" ] }, + "node_modules/@verdaccio/auth": { + "version": "8.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/@verdaccio/auth/-/auth-8.0.0-next-8.29.tgz", + "integrity": "sha512-lEIqneZ7LYYkF07/P9cJhOYVZTIIARGRVE66j666VR58T/fY/pC4hxEfuyqlfd3BhS3hM65aIrPZu+s03hyg+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/config": "8.0.0-next-8.29", + "@verdaccio/core": "8.0.0-next-8.29", + "@verdaccio/loaders": "8.0.0-next-8.19", + "@verdaccio/signature": "8.0.0-next-8.21", + "debug": "4.4.3", + "lodash": "4.17.21", + "verdaccio-htpasswd": "13.0.0-next-8.29" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/auth/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@verdaccio/config": { + "version": "8.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/@verdaccio/config/-/config-8.0.0-next-8.29.tgz", + "integrity": "sha512-7kEZ6tv3NsJFPcKQrxELMkvJFI9Hmy1s8IPjuXjz9gobDV3NyF9VhPsh6l18Tm+nsqXb6ZTWiRup9V8Lkg0p0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.29", + "debug": "4.4.3", + "js-yaml": "4.1.1", + "lodash": "4.17.21" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/config/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@verdaccio/core": { + "version": "8.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/@verdaccio/core/-/core-8.0.0-next-8.29.tgz", + "integrity": "sha512-ztsNbnHMGqpOJlC3x/Jz7+0Xzrul+UIQQAFsTFHO3pET8nyZWkh/1TH50olz0pZ/OS87O/+7mk04TK9hHD/eFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "http-errors": "2.0.0", + "http-status-codes": "2.3.0", + "minimatch": "7.4.6", + "process-warning": "1.0.0", + "semver": "7.7.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/core/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@verdaccio/core/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@verdaccio/core/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@verdaccio/core/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@verdaccio/file-locking": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@verdaccio/file-locking/-/file-locking-10.3.1.tgz", + "integrity": "sha512-oqYLfv3Yg3mAgw9qhASBpjD50osj2AX4IwbkUtyuhhKGyoFU9eZdrbeW6tpnqUnj6yBMtAPm2eGD4BwQuX400g==", + "dev": true, + "license": "MIT", + "dependencies": { + "lockfile": "1.0.4" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/hooks": { + "version": "8.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/@verdaccio/hooks/-/hooks-8.0.0-next-8.29.tgz", + "integrity": "sha512-KRuTN6iYg9ntFXmrH+fY+dNCFpNBMcEwOVLOJXk/fA3I3ki188FpRZz9rKToyj1XYZzPDqGUWv82ekswX5wrYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.29", + "@verdaccio/logger": "8.0.0-next-8.29", + "debug": "4.4.3", + "got-cjs": "12.5.4", + "handlebars": "4.7.8" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/loaders": { + "version": "8.0.0-next-8.19", + "resolved": "https://registry.npmjs.org/@verdaccio/loaders/-/loaders-8.0.0-next-8.19.tgz", + "integrity": "sha512-+mQuEZNLRZ4EEjzfROHrVeZXVHNFhQTpI98KCpcbU1NEC7ZAl5m7OBD2XPVtvNTia2ZT83q4H0JPi/bAomnIRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.29", + "debug": "4.4.3", + "lodash": "4.17.21" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/loaders/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@verdaccio/local-storage-legacy": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@verdaccio/local-storage-legacy/-/local-storage-legacy-11.1.1.tgz", + "integrity": "sha512-P6ahH2W6/KqfJFKP+Eid7P134FHDLNvHa+i8KVgRVBeo2/IXb6FEANpM1mCVNvPANu0LCAmNJBOXweoUKViaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.21", + "@verdaccio/file-locking": "10.3.1", + "@verdaccio/streams": "10.2.1", + "async": "3.2.6", + "debug": "4.4.1", + "lodash": "4.17.21", + "lowdb": "1.0.0", + "mkdirp": "1.0.4" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/local-storage-legacy/node_modules/@verdaccio/core": { + "version": "8.0.0-next-8.21", + "resolved": "https://registry.npmjs.org/@verdaccio/core/-/core-8.0.0-next-8.21.tgz", + "integrity": "sha512-n3Y8cqf84cwXxUUdTTfEJc8fV55PONPKijCt2YaC0jilb5qp1ieB3d4brqTOdCdXuwkmnG2uLCiGpUd/RuSW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "http-errors": "2.0.0", + "http-status-codes": "2.3.0", + "minimatch": "7.4.6", + "process-warning": "1.0.0", + "semver": "7.7.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/local-storage-legacy/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@verdaccio/local-storage-legacy/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@verdaccio/local-storage-legacy/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@verdaccio/local-storage-legacy/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@verdaccio/local-storage-legacy/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@verdaccio/local-storage-legacy/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@verdaccio/local-storage-legacy/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@verdaccio/logger": { + "version": "8.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/@verdaccio/logger/-/logger-8.0.0-next-8.29.tgz", + "integrity": "sha512-AF6hBAkgsRjDFGLjFWO9hzUWM47Jt15v90I6Wk0TotzsrWxgBYbslfJyTgwmTpGjhPe7al2dnFwT7hxKa6fr/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/logger-commons": "8.0.0-next-8.29", + "pino": "9.14.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/logger-commons": { + "version": "8.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/@verdaccio/logger-commons/-/logger-commons-8.0.0-next-8.29.tgz", + "integrity": "sha512-CVeLv+U0cL9wtNJVwtzjj7wtUFYxMDmynWoXtepP+AthVoNj6G6rU2P2tGLX/uqH40EwqaHJy5Vo0uZ3ex2qhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.29", + "@verdaccio/logger-prettify": "8.0.0-next-8.4", + "colorette": "2.0.20", + "debug": "4.4.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/logger-prettify": { + "version": "8.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/logger-prettify/-/logger-prettify-8.0.0-next-8.4.tgz", + "integrity": "sha512-gjI/JW29fyalutn/X1PQ0iNuGvzeVWKXRmnLa7gXVKhdi4p37l/j7YZ7n44XVbbiLIKAK0pbavEg9Yr66QrYaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "2.0.20", + "dayjs": "1.11.13", + "lodash": "4.17.21", + "on-exit-leak-free": "2.1.2", + "pino-abstract-transport": "1.2.0", + "sonic-boom": "3.8.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/logger-prettify/node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@verdaccio/logger-prettify/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@verdaccio/middleware": { + "version": "8.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/@verdaccio/middleware/-/middleware-8.0.0-next-8.29.tgz", + "integrity": "sha512-ANsQ8qjyNslH6BfGZnNkBvK1acTlnFgedXen6BnbN9nF+AWzHUea6knh529J5Clm3zrWggbRnGCrp1SSlbOqSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/config": "8.0.0-next-8.29", + "@verdaccio/core": "8.0.0-next-8.29", + "@verdaccio/url": "13.0.0-next-8.29", + "debug": "4.4.3", + "express": "4.22.1", + "express-rate-limit": "5.5.1", + "lodash": "4.17.21", + "lru-cache": "7.18.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/middleware/node_modules/express-rate-limit": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz", + "integrity": "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@verdaccio/middleware/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@verdaccio/middleware/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@verdaccio/search-indexer": { + "version": "8.0.0-next-8.5", + "resolved": "https://registry.npmjs.org/@verdaccio/search-indexer/-/search-indexer-8.0.0-next-8.5.tgz", + "integrity": "sha512-0GC2tJKstbPg/W2PZl2yE+hoAxffD2ZWilEnEYSEo2e9UQpNIy2zg7KE/uMUq2P72Vf5EVfVzb8jdaH4KV4QeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/signature": { + "version": "8.0.0-next-8.21", + "resolved": "https://registry.npmjs.org/@verdaccio/signature/-/signature-8.0.0-next-8.21.tgz", + "integrity": "sha512-5TuGPRT4c5B3k6svxJ6Q5l31UBZTODo3iAJxmuAAC0iZBLNnivdvJlqUVdpZtYfrhwvuP5vuo7lucHQk6DsSmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/config": "8.0.0-next-8.29", + "@verdaccio/core": "8.0.0-next-8.29", + "debug": "4.4.3", + "jsonwebtoken": "9.0.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/streams": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@verdaccio/streams/-/streams-10.2.1.tgz", + "integrity": "sha512-OojIG/f7UYKxC4dYX8x5ax8QhRx1b8OYUAMz82rUottCuzrssX/4nn5QE7Ank0DUSX3C9l/HPthc4d9uKRJqJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/tarball": { + "version": "13.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/@verdaccio/tarball/-/tarball-13.0.0-next-8.29.tgz", + "integrity": "sha512-4hTQMXYF1olwwydaVfb6ab1TTImM42rAxLmw3VnJUI5ttbmfB9h095/TYsCssy5vqGQMp3l8YW/JxVTPcVtGYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.29", + "@verdaccio/url": "13.0.0-next-8.29", + "debug": "4.4.3", + "gunzip-maybe": "1.4.2", + "tar-stream": "3.1.7" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/ui-theme": { + "version": "8.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/@verdaccio/ui-theme/-/ui-theme-8.0.0-next-8.29.tgz", + "integrity": "sha512-kSg69so1LHOL2SA1qLJikFxkx8FEOFQJV+Nde0lN3XMkp0RST8DgLx/hiVbgYzPz54S8LuvYR/DQAulyRulMcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@verdaccio/url": { + "version": "13.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/@verdaccio/url/-/url-13.0.0-next-8.29.tgz", + "integrity": "sha512-ucJ6MhLfY0g8uU0zjcJypSkCa1mXSLmrqdiILyj16Lo0y4w/gEvddMrfab8QUmgvvuzbCgSqCZHJmbZn7DFxQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.29", + "debug": "4.4.3", + "validator": "13.15.26" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/utils": { + "version": "8.1.0-next-8.29", + "resolved": "https://registry.npmjs.org/@verdaccio/utils/-/utils-8.1.0-next-8.29.tgz", + "integrity": "sha512-EgyazlL0VejvZqWZ6KL3ig27Yl8RXcwhz1hayuqeAIxaqbsnmSmogL2zKXgGnm9y/A6QkPfZH1BcQoUh1STvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.29", + "lodash": "4.17.21", + "minimatch": "7.4.6" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/utils/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@verdaccio/utils/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@verdaccio/utils/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", @@ -22362,6 +23039,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -22408,6 +23105,16 @@ "node": ">= 4.0.0" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.23", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", @@ -22445,6 +23152,23 @@ "postcss": "^8.1.0" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, "node_modules/axe-core": { "version": "4.11.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", @@ -22818,6 +23542,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -23065,6 +23799,23 @@ "node": ">=8" } }, + "node_modules/browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "~0.2.0" + } + }, + "node_modules/browserify-zlib/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true, + "license": "MIT" + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -23169,6 +23920,13 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -23485,6 +24243,13 @@ "node": ">=4" } }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -23859,6 +24624,22 @@ "node": ">= 12" } }, + "node_modules/clipanion": { + "version": "4.0.0-rc.4", + "resolved": "https://registry.npmjs.org/clipanion/-/clipanion-4.0.0-rc.4.tgz", + "integrity": "sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q==", + "dev": true, + "license": "MIT", + "workspaces": [ + "website" + ], + "dependencies": { + "typanion": "^3.8.0" + }, + "peerDependencies": { + "typanion": "*" + } + }, "node_modules/clipboard": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", @@ -23967,6 +24748,29 @@ "node": ">=0.10.0" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -25653,6 +26457,19 @@ "lodash-es": "^4.17.21" } }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -26391,6 +27208,52 @@ "dev": true, "license": "MIT" }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/earcut": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", @@ -26404,6 +27267,27 @@ "dev": true, "license": "MIT" }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -26589,6 +27473,19 @@ "node": ">=6" } }, + "node_modules/envinfo": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.15.0.tgz", + "integrity": "sha512-chR+t7exF6y59kelhXw5I3849nTy7KIRO+ePdLMhCD+JRP/JvmkenDWP7QSFGlsHX+kxGxdDutOPrmj5j1HR6g==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -27717,6 +28614,23 @@ "node": ">=4" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, "node_modules/fancy-log": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-2.0.0.tgz", @@ -28334,6 +29248,16 @@ "dev": true, "license": "ISC" }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "7.2.13", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.13.tgz", @@ -28916,6 +29840,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -29179,6 +30113,157 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "node_modules/got-cjs": { + "version": "12.5.4", + "resolved": "https://registry.npmjs.org/got-cjs/-/got-cjs-12.5.4.tgz", + "integrity": "sha512-Uas6lAsP8bRCt5WXGMhjFf/qEHTrm4v4qxGR02rLG2kdG9qedctvlkdwXVcDJ7Cs84X+r4dPU7vdwGjCaspXug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "4.6.0", + "@szmarczak/http-timer": "4.0.6", + "@types/responselike": "1.0.0", + "cacheable-lookup": "6.1.0", + "cacheable-request": "7.0.2", + "decompress-response": "^6.0.0", + "form-data-encoder": "1.7.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "2.0.0", + "p-cancelable": "2.1.1", + "responselike": "2.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got-cjs/node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/got-cjs/node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/got-cjs/node_modules/cacheable-lookup": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.1.0.tgz", + "integrity": "sha512-KJ/Dmo1lDDhmW2XDPMo+9oiy/CeqosPguPCrgcVzKyZrL6pM1gU2GmPY/xo6OQPTUaA/c0kwHuywB4E6nmT9ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/got-cjs/node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/got-cjs/node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got-cjs/node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/got-cjs/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/got-cjs/node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got-cjs/node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/got-cjs/node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -29202,6 +30287,24 @@ "dev": true, "license": "MIT" }, + "node_modules/gunzip-maybe": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", + "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-zlib": "^0.1.4", + "is-deflate": "^1.0.0", + "is-gzip": "^1.0.0", + "peek-stream": "^1.1.0", + "pumpify": "^1.3.3", + "through2": "^2.0.3" + }, + "bin": { + "gunzip-maybe": "bin.js" + } + }, "node_modules/h3-js": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/h3-js/-/h3-js-3.7.2.tgz", @@ -29836,6 +30939,28 @@ "node": ">=12" } }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "dev": true, + "license": "MIT" + }, "node_modules/http2-wrapper": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", @@ -30307,6 +31432,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-deflate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", + "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -30370,6 +31502,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-gzip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", + "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-in-ssh": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", @@ -30571,6 +31713,13 @@ "ws": "*" } }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -32202,6 +33351,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, "node_modules/jsdom": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", @@ -32311,6 +33467,13 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-migrate-x": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/json-schema-migrate-x/-/json-schema-migrate-x-2.1.0.tgz", @@ -32354,6 +33517,13 @@ "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -32528,6 +33698,46 @@ ], "license": "MIT" }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jspreadsheet-ce": { "version": "4.15.0", "resolved": "https://registry.npmjs.org/jspreadsheet-ce/-/jspreadsheet-ce-4.15.0.tgz", @@ -32537,11 +33747,50 @@ "jsuites": "^5.6.5" } }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, "node_modules/jsuites": { "version": "5.13.5", "resolved": "https://registry.npmjs.org/jsuites/-/jsuites-5.13.5.tgz", "integrity": "sha512-cvkcpy/v5I3+IAcNPE4UP38PFCEfUQw9JI5NN61dlcXLwkD+2UTIOsRPvgMLeqI1eDWHL4AHfrbcE/+TFciUsw==" }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -33173,6 +34422,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lockfile": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", + "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "dev": true, + "license": "ISC", + "dependencies": { + "signal-exit": "^3.0.2" + } + }, + "node_modules/lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -33207,6 +34473,48 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -33221,6 +34529,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -33446,6 +34761,33 @@ "dev": true, "license": "MIT" }, + "node_modules/lowdb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", + "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.3", + "is-promise": "^2.1.0", + "lodash": "4", + "pify": "^3.0.0", + "steno": "^0.4.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lowdb/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -52437,6 +53779,16 @@ "dev": true, "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -53325,6 +54677,18 @@ "pbf": "bin/pbf" } }, + "node_modules/peek-stream": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", + "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "duplexify": "^3.5.0", + "through2": "^2.0.3" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -53332,6 +54696,13 @@ "dev": true, "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -53375,6 +54746,84 @@ "node": ">=0.10.0" } }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pino/node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pino/node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -54377,6 +55826,13 @@ "node": ">=8" } }, + "node_modules/process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -54488,6 +55944,29 @@ "once": "^1.3.1" } }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -54561,6 +56040,13 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, "node_modules/quick-lru": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", @@ -54825,6 +56311,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -55629,6 +57125,16 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -56896,6 +58402,16 @@ "node": ">= 14" } }, + "node_modules/sonic-boom": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -57169,6 +58685,16 @@ "node": "*" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -57187,6 +58713,32 @@ "node": ">=0.8" } }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ssri": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", @@ -57252,6 +58804,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/steno": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", + "integrity": "sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.3" + } + }, "node_modules/store2": { "version": "2.14.4", "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.4.tgz", @@ -57388,6 +58950,13 @@ "through": "~2.3.4" } }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/streamroller": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", @@ -58261,6 +59830,16 @@ "tslib": "^2" } }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/three": { "version": "0.172.0", "resolved": "https://registry.npmjs.org/three/-/three-0.172.0.tgz", @@ -58274,6 +59853,50 @@ "dev": true, "license": "MIT" }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -59053,6 +60676,23 @@ "integrity": "sha512-Ja03QIJlPuHt4IQ2FfGex4F4JAr8m3jpaHbFbQrgwr7s7L6U8ocrHiF3J1+wf9jzhGKxvDeaCAnGDot8OjGFyA==", "license": "(EDL-1.0 OR EPL-1.0)" }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/typanion": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/typanion/-/typanion-3.14.0.tgz", + "integrity": "sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==", + "dev": true, + "license": "MIT", + "workspaces": [ + "website" + ] + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -59656,6 +61296,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/varint": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", @@ -60188,6 +61838,228 @@ "vega-util": "^2.1.0" } }, + "node_modules/verdaccio": { + "version": "6.2.9", + "resolved": "https://registry.npmjs.org/verdaccio/-/verdaccio-6.2.9.tgz", + "integrity": "sha512-w1LYqM/wuvtiUedF9eSTsIC1yEI0nShIX48OqG1R6xzS4eEt0Pe1NYl5oPC/d7UyVSOufpjE8QVgd1CqauyhXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cypress/request": "3.0.10", + "@verdaccio/auth": "8.0.0-next-8.29", + "@verdaccio/config": "8.0.0-next-8.29", + "@verdaccio/core": "8.0.0-next-8.29", + "@verdaccio/hooks": "8.0.0-next-8.29", + "@verdaccio/loaders": "8.0.0-next-8.19", + "@verdaccio/local-storage-legacy": "11.1.1", + "@verdaccio/logger": "8.0.0-next-8.29", + "@verdaccio/middleware": "8.0.0-next-8.29", + "@verdaccio/search-indexer": "8.0.0-next-8.5", + "@verdaccio/signature": "8.0.0-next-8.21", + "@verdaccio/streams": "10.2.1", + "@verdaccio/tarball": "13.0.0-next-8.29", + "@verdaccio/ui-theme": "8.0.0-next-8.29", + "@verdaccio/url": "13.0.0-next-8.29", + "@verdaccio/utils": "8.1.0-next-8.29", + "async": "3.2.6", + "clipanion": "4.0.0-rc.4", + "compression": "1.8.1", + "cors": "2.8.6", + "debug": "4.4.3", + "envinfo": "7.15.0", + "express": "4.22.1", + "JSONStream": "1.3.5", + "lodash": "4.17.23", + "lru-cache": "7.18.3", + "mime": "3.0.0", + "semver": "7.7.4", + "verdaccio-audit": "13.0.0-next-8.29", + "verdaccio-htpasswd": "13.0.0-next-8.29" + }, + "bin": { + "verdaccio": "bin/verdaccio" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/verdaccio-audit": { + "version": "13.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/verdaccio-audit/-/verdaccio-audit-13.0.0-next-8.29.tgz", + "integrity": "sha512-ZUNONewbFocBq3oWXrwAL8IX4ZovPU70yj0nuYStSVJ9+Vrb74Duc+eI+IIS+jLfyysZe5L0ZAODGN8ny1Lu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/config": "8.0.0-next-8.29", + "@verdaccio/core": "8.0.0-next-8.29", + "express": "4.22.1", + "https-proxy-agent": "5.0.1", + "node-fetch": "cjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/verdaccio-audit/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/verdaccio-audit/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/verdaccio-htpasswd": { + "version": "13.0.0-next-8.29", + "resolved": "https://registry.npmjs.org/verdaccio-htpasswd/-/verdaccio-htpasswd-13.0.0-next-8.29.tgz", + "integrity": "sha512-JBYCaSTQSUws/EXOqNrh7iOyWPrGLTYSeufCS3Y6BOCJbfDiy2Nh8PbstoZn/L9ZbzUesjPPiIZ4Ou3eUaK0Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.29", + "@verdaccio/file-locking": "13.0.0-next-8.6", + "apache-md5": "1.1.8", + "bcryptjs": "2.4.3", + "debug": "4.4.3", + "http-errors": "2.0.0", + "unix-crypt-td-js": "1.1.4" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/verdaccio-htpasswd/node_modules/@verdaccio/file-locking": { + "version": "13.0.0-next-8.6", + "resolved": "https://registry.npmjs.org/@verdaccio/file-locking/-/file-locking-13.0.0-next-8.6.tgz", + "integrity": "sha512-F6xQWvsZnEyGjugrYfe+D/ChSVudXmBFWi8xuTIX6PAdp7dk9x9biOGQFW8O3GSAK8UhJ6WlRisQKJeYRa6vWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lockfile": "1.0.4" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/verdaccio-htpasswd/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verdaccio-htpasswd/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verdaccio/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/verdaccio/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/verdaccio/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, "node_modules/viewport-mercator-project": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/viewport-mercator-project/-/viewport-mercator-project-7.0.4.tgz", @@ -61522,6 +63394,16 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index aa4538a098..a6a99cae4a 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "@storybook/test-runner": "^0.24.2", "@swc-node/register": "^1.11.1", "@swc/cli": "^0.8.0", - "@swc/core": "^1.11.1", + "@swc/core": "~1.15.5", "@swc/helpers": "^0.5.18", "@testing-library/angular": "^19.1.0", "@testing-library/dom": "^10.4.1", @@ -200,6 +200,7 @@ "ts-node": "^10.9.2", "typescript": "5.9.3", "typescript-eslint": "^8.56.0", + "verdaccio": "^6.0.5", "webpack": "5.105.2" } } diff --git a/project.json b/project.json index ecf53dce56..3266e36403 100644 --- a/project.json +++ b/project.json @@ -17,6 +17,14 @@ "params": "forward" } ] + }, + "local-registry": { + "executor": "@nx/js:verdaccio", + "options": { + "port": 4873, + "config": ".verdaccio/config.yml", + "storage": "tmp/local-registry/storage" + } } } } diff --git a/tsconfig.base.json b/tsconfig.base.json index a0a8830f74..afe4688ef9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -6,14 +6,8 @@ "target": "ES2022", "module": "esnext", "moduleResolution": "bundler", - "lib": [ - "ES2022", - "DOM" - ], - "typeRoots": [ - "./node_modules/@types", - "./node_modules/@testing-library" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "typeRoots": ["./node_modules/@types", "./node_modules/@testing-library"], "allowSyntheticDefaultImports": true, "declaration": false, "esModuleInterop": true, @@ -32,87 +26,47 @@ "useDefineForClassFields": true, "useUnknownInCatchVariables": true, "paths": { - "@hra-ui/application": [ - "libs/application/src/index.ts" - ], - "@hra-ui/cde-visualization": [ - "libs/cde-visualization/src/index.ts" - ], - "@hra-ui/cdk": [ - "libs/cdk/src/index.ts" - ], - "@hra-ui/cdk/*": [ - "libs/cdk/*/src/index.ts" - ], - "@hra-ui/common": [ - "libs/common/src/index.ts" - ], - "@hra-ui/common/*": [ - "libs/common/*/src/index.ts" - ], - "@hra-ui/components/*": [ - "libs/components/*/src/index.ts" - ], - "@hra-ui/dashboard": [ - "libs/dashboard/src/index.ts" - ], - "@hra-ui/design-system": [ - "libs/design-system/src/index.ts" - ], - "@hra-ui/design-system/*": [ - "libs/design-system/*/src/index.ts" - ], - "@hra-ui/ftu-ui-components": [ - "libs/ftu-ui-components/src/index.ts" - ], - "@hra-ui/ftu-ui-components/*": [ - "libs/ftu-ui-components/*/src/index.ts" - ], - "@hra-ui/node-dist-vis": [ - "libs/node-dist-vis/src/index.ts" - ], - "@hra-ui/node-dist-vis/*": [ - "libs/node-dist-vis/*/src/index.ts" - ], - "@hra-ui/services": [ - "libs/services/src/index.ts" - ], - "@hra-ui/state": [ - "libs/state/src/index.ts" - ], - "@hra-ui/utils": [ - "libs/shared/utils/src/index.ts" - ], - "@hra-ui/utils/*": [ - "libs/shared/utils/*/src/index.ts" - ], - "@hra-ui/webcomponents": [ - "libs/shared/webcomponents/src/index.ts" - ], - "@hra-ui/zod-to-json-schema": [ - "tools/plugins/zod-to-json-schema/src/index.ts" - ], - "ccf-body-ui": [ - "libs/ccf-body-ui/src/public-api.ts" - ], - "ccf-scene-utils": [ - "libs/ccf-scene-utils/src/index.ts" - ], - "ccf-shared": [ - "libs/ccf-shared/src/public-api.ts" - ], - "ccf-shared/*": [ - "libs/ccf-shared/*/src/public-api.ts" - ], - "vega-embed": [ - "node_modules/vega-embed/build/embed.d.ts" - ] + "@hra-ui/analytics": ["libs/hra-ui/analytics/src/index.ts"], + "@hra-ui/analytics/*": ["libs/hra-ui/analytics/*/src/index.ts"], + "@hra-ui/application": ["libs/application/src/index.ts"], + "@hra-ui/application-v2": ["libs/hra-ui/application/src/index.ts"], + "@hra-ui/application-v2/*": ["libs/hra-ui/application/*/src/index.ts"], + "@hra-ui/cde-visualization": ["libs/cde-visualization/src/index.ts"], + "@hra-ui/cdk": ["libs/cdk/src/index.ts"], + "@hra-ui/cdk/*": ["libs/cdk/*/src/index.ts"], + "@hra-ui/cdk-v2": ["libs/hra-ui/cdk/src/index.ts"], + "@hra-ui/cdk-v2/*": ["libs/hra-ui/cdk/*/src/index.ts"], + "@hra-ui/common": ["libs/common/src/index.ts"], + "@hra-ui/common/*": ["libs/common/*/src/index.ts"], + "@hra-ui/common-v2": ["libs/hra-ui/common/src/index.ts"], + "@hra-ui/common-v2/*": ["libs/hra-ui/common/*/src/index.ts"], + "@hra-ui/content-templates": ["libs/hra-ui/content-templates/src/index.ts"], + "@hra-ui/content-templates/*": ["libs/hra-ui/content-templates/*/src/index.ts"], + "@hra-ui/dashboard": ["libs/dashboard/src/index.ts"], + "@hra-ui/design-system": ["libs/design-system/src/index.ts"], + "@hra-ui/design-system/*": ["libs/design-system/*/src/index.ts"], + "@hra-ui/design-system-v2": ["libs/hra-ui/design-system/src/index.ts"], + "@hra-ui/design-system-v2/*": ["libs/hra-ui/design-system/*/src/index.ts"], + "@hra-ui/ftu-ui-components": ["libs/ftu-ui-components/src/index.ts"], + "@hra-ui/ftu-ui-components/*": ["libs/ftu-ui-components/*/src/index.ts"], + "@hra-ui/node-dist-vis": ["libs/node-dist-vis/src/index.ts"], + "@hra-ui/node-dist-vis/*": ["libs/node-dist-vis/*/src/index.ts"], + "@hra-ui/router-ext": ["libs/hra-ui/router-ext/src/index.ts"], + "@hra-ui/router-ext/*": ["libs/hra-ui/router-ext/*/src/index.ts"], + "@hra-ui/services": ["libs/services/src/index.ts"], + "@hra-ui/state": ["libs/state/src/index.ts"], + "@hra-ui/utils": ["libs/hra-ui/utils/src/index.ts"], + "@hra-ui/utils/*": ["libs/hra-ui/utils/*/src/index.ts"], + "@hra-ui/webcomponents": ["libs/shared/webcomponents/src/index.ts"], + "@hra-ui/zod-to-json-schema": ["tools/plugins/zod-to-json-schema/src/index.ts"], + "ccf-body-ui": ["libs/ccf-body-ui/src/public-api.ts"], + "ccf-scene-utils": ["libs/ccf-scene-utils/src/index.ts"], + "ccf-shared": ["libs/ccf-shared/src/public-api.ts"], + "ccf-shared/*": ["libs/ccf-shared/*/src/public-api.ts"], + "vega-embed": ["node_modules/vega-embed/build/embed.d.ts"] } }, - "exclude": [ - "node_modules", - "tmp" - ], + "exclude": ["node_modules", "tmp"], "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true,