From 87c233dba537fa34385c1bf77195f2d5879a97d1 Mon Sep 17 00:00:00 2001 From: Jonatan Svennberg Date: Wed, 6 May 2026 19:19:22 +0200 Subject: [PATCH 01/12] Migrated comment moderation filters to new filter abstraction (#27043) ref https://linear.app/ghost/issue/BER-3505/ ## What changed This reworks comment moderation filters in `apps/posts` to use the same canonical field/codec/NQL pipeline as members instead of the old ad-hoc query-param implementation. It also keeps `id` and `thread` out of filter state: - `filter` is now the only concern of the comment filter-state hook - legacy `?id=is:` deep links are handled separately at the page level - single-comment mode still clears back to the full comments view Follow-up work on this branch also tightened the shared value-source behavior so selected author/post filters remain representable when the backing record cannot be hydrated, without allowing synthetic `ID: ...` placeholders to win over real hydrated labels in the combined post/page source. ## Why The previous comments filter implementation had a hand-rolled `buildNqlFilter()` path, custom URL shape, no NQL parsing, and browser-local date handling. That had drifted away from the mainline filter abstractions and made comments harder to reason about and maintain. This brings comments back onto the current shared model while keeping the deep-link behavior that moderation flows still rely on. ## User impact - comment moderation filters now round-trip through canonical `?filter=` URLs - filter parsing/serialization is aligned with members - exact-date comment filtering uses site timezone semantics instead of browser-local assumptions - legacy single-comment links via `?id=is:` still work - missing selected author/post resources stay visible in filter UI instead of collapsing into a broken state --- apps/posts/package.json | 1 + .../create-combined-value-source.ts | 17 +- .../create-ghost-browse-value-source.ts | 3 + .../create-remote-value-source.ts | 30 ++- .../filter-sources/use-member-value-source.ts | 4 + .../use-post-resource-value-source.ts | 6 +- .../src/views/comments/comment-fields.ts | 129 +++++++++ .../views/comments/comment-filter-query.ts | 38 +++ apps/posts/src/views/comments/comments.tsx | 125 +++++++-- .../comments/components/comments-filters.tsx | 102 +------ .../views/comments/hooks/use-filter-state.ts | 254 +++++------------- .../comments/legacy-comment-filter-query.ts | 61 +++++ .../comments/use-comment-filter-fields.ts | 59 ++++ .../src/views/filters/filter-codecs.test.ts | 47 +++- apps/posts/src/views/filters/filter-codecs.ts | 72 ++++- apps/posts/src/views/filters/filter-date.ts | 10 + .../filters/filter-normalization.test.ts | 12 - .../src/views/filters/filter-normalization.ts | 47 ++++ .../views/filters/filter-operator-options.ts | 20 ++ .../views/filters/filter-query-core.test.ts | 19 +- .../src/views/filters/filter-query-core.ts | 32 +++ .../src/views/members/member-fields.test.ts | 2 +- apps/posts/src/views/members/member-fields.ts | 108 +------- .../src/views/members/member-filter-query.ts | 33 +-- .../views/members/use-member-filter-fields.ts | 28 +- .../create-combined-value-source.test.tsx | 111 ++++++++ .../hooks/create-remote-value-source.test.tsx | 58 ++++ .../unit/utils/filter-normalization.test.ts | 39 +++ .../views/comments/comment-fields.test.ts | 143 ++++++++++ .../comments/comment-filter-query.test.ts | 175 ++++++++++++ .../use-comment-filter-fields.test.tsx | 43 +++ .../views/comments/use-filter-state.test.tsx | 171 ++++++++++++ pnpm-lock.yaml | 15 ++ 33 files changed, 1551 insertions(+), 463 deletions(-) create mode 100644 apps/posts/src/views/comments/comment-fields.ts create mode 100644 apps/posts/src/views/comments/comment-filter-query.ts create mode 100644 apps/posts/src/views/comments/legacy-comment-filter-query.ts create mode 100644 apps/posts/src/views/comments/use-comment-filter-fields.ts create mode 100644 apps/posts/src/views/filters/filter-date.ts delete mode 100644 apps/posts/src/views/filters/filter-normalization.test.ts create mode 100644 apps/posts/src/views/filters/filter-operator-options.ts create mode 100644 apps/posts/test/unit/hooks/create-combined-value-source.test.tsx create mode 100644 apps/posts/test/unit/hooks/create-remote-value-source.test.tsx create mode 100644 apps/posts/test/unit/utils/filter-normalization.test.ts create mode 100644 apps/posts/test/unit/views/comments/comment-fields.test.ts create mode 100644 apps/posts/test/unit/views/comments/comment-filter-query.test.ts create mode 100644 apps/posts/test/unit/views/comments/use-comment-filter-fields.test.tsx create mode 100644 apps/posts/test/unit/views/comments/use-filter-state.test.tsx diff --git a/apps/posts/package.json b/apps/posts/package.json index 5c993f8ca67..1da2fe6a3c2 100644 --- a/apps/posts/package.json +++ b/apps/posts/package.json @@ -62,6 +62,7 @@ "react-dom": "18.3.1", "react-router": "7.14.0", "sonner": "2.0.7", + "temporal-polyfill": "0.3.0", "use-debounce": "10.1.1", "zod": "4.1.12" }, diff --git a/apps/posts/src/hooks/filter-sources/create-combined-value-source.ts b/apps/posts/src/hooks/filter-sources/create-combined-value-source.ts index d6a645358f0..ee2f4e71fc0 100644 --- a/apps/posts/src/hooks/filter-sources/create-combined-value-source.ts +++ b/apps/posts/src/hooks/filter-sources/create-combined-value-source.ts @@ -1,11 +1,12 @@ -import {ValueSource, ValueSourceParams, ValueSourceState} from '@tryghost/shade/patterns'; +import {FilterOption, ValueSource, ValueSourceParams, ValueSourceState} from '@tryghost/shade/patterns'; import {ValueSourceHook, ValueSourceHookOptions} from './create-remote-value-source'; import {mergeFilterOptions} from './utils'; import {useCallback, useMemo} from 'react'; export function createCombinedValueSource( useFirstSource: ValueSourceHook, - useSecondSource: ValueSourceHook + useSecondSource: ValueSourceHook, + getMissingSelectedOption?: (selectedValue: T) => FilterOption ): ValueSourceHook { return function useCombinedValueSource(options?: ValueSourceHookOptions): ValueSource { const firstSource = useFirstSource(options); @@ -14,9 +15,19 @@ export function createCombinedValueSource( const useOptions = useCallback(({query, selectedValues}: ValueSourceParams): ValueSourceState => { const firstState = firstSource.useOptions({query, selectedValues}); const secondState = secondSource.useOptions({query, selectedValues}); + const mergedOptions = mergeFilterOptions(firstState.options, secondState.options); + const fallbackOptions = getMissingSelectedOption ? selectedValues.flatMap((selectedValue) => { + const hasMatch = mergedOptions.some(option => option.value === selectedValue); + + if (hasMatch) { + return []; + } + + return [getMissingSelectedOption(selectedValue)]; + }) : []; return { - options: mergeFilterOptions(firstState.options, secondState.options), + options: mergeFilterOptions(mergedOptions, fallbackOptions), isInitialLoad: firstState.options.length === 0 && secondState.options.length === 0 && (firstState.isInitialLoad || secondState.isInitialLoad), diff --git a/apps/posts/src/hooks/filter-sources/create-ghost-browse-value-source.ts b/apps/posts/src/hooks/filter-sources/create-ghost-browse-value-source.ts index 0ceb3d88b39..573f1dd8166 100644 --- a/apps/posts/src/hooks/filter-sources/create-ghost-browse-value-source.ts +++ b/apps/posts/src/hooks/filter-sources/create-ghost-browse-value-source.ts @@ -25,6 +25,7 @@ interface CreateGhostBrowseValueSourceConfig { debounceMs?: number; selectItems: (data: Data | undefined) => Item[] | undefined; toOption: (item: Item) => FilterOption; + getMissingSelectedOption?: (selectedValue: string) => FilterOption; useQuery: ( options: {enabled: boolean; searchParams: Record} ) => InfiniteBrowseResult; @@ -53,6 +54,7 @@ export function createGhostBrowseValueSource) { return createRemoteValueSource({ @@ -93,6 +95,7 @@ export function createGhostBrowseValueSource { useBrowse: (query: string, options: ValueSourceHookOptions) => BrowseState; useHydrate?: (selectedValues: T[], options: ValueSourceHookOptions) => HydrateState; toOption: (item: Item) => FilterOption; + getMissingSelectedOption?: (selectedValue: T) => FilterOption; debounceMs?: number; } @@ -52,6 +53,26 @@ export type RemoteValueSource = ValueSource & { export type RemoteValueSourceHook = (options?: ValueSourceHookOptions) => RemoteValueSource; +function buildFallbackOptions( + selectedValues: T[], + mergedOptions: FilterOption[], + getMissingSelectedOption?: (selectedValue: T) => FilterOption +): FilterOption[] { + if (!getMissingSelectedOption) { + return []; + } + + return selectedValues.flatMap((selectedValue) => { + const hasMatch = mergedOptions.some(option => option.value === selectedValue); + + if (hasMatch) { + return []; + } + + return [getMissingSelectedOption(selectedValue)]; + }); +} + export function createRemoteValueSource( config: RemoteValueSourceConfig ): RemoteValueSourceHook { @@ -88,6 +109,13 @@ export function createRemoteValueSource( const mergedOptions = useMemo(() => { return mergeFilterOptions(hydratedOptions, visibleOptions); }, [hydratedOptions, visibleOptions]); + const fallbackOptions = useMemo(() => { + return buildFallbackOptions( + selectedValues, + mergedOptions, + config.getMissingSelectedOption + ); + }, [mergedOptions, selectedValues]); if (!enabled) { return { @@ -101,7 +129,7 @@ export function createRemoteValueSource( } return { - options: mergedOptions, + options: mergeFilterOptions(fallbackOptions, mergedOptions), isInitialLoad: browse.isLoading && mergedOptions.length === 0, isSearching: !browse.isLoading && browse.isRefreshing && !browse.isLoadingMore, isLoadingMore: browse.isLoadingMore, diff --git a/apps/posts/src/hooks/filter-sources/use-member-value-source.ts b/apps/posts/src/hooks/filter-sources/use-member-value-source.ts index 86cb7826b0c..79ed9e7b225 100644 --- a/apps/posts/src/hooks/filter-sources/use-member-value-source.ts +++ b/apps/posts/src/hooks/filter-sources/use-member-value-source.ts @@ -17,6 +17,10 @@ const useRemoteMemberValueSource = createGhostBrowseValueSource ({ + value, + label: `ID: ${value}` + }), selectItems: data => data?.members, useQuery: ({enabled, searchParams}) => { return useBrowseMembersInfinite({ diff --git a/apps/posts/src/hooks/filter-sources/use-post-resource-value-source.ts b/apps/posts/src/hooks/filter-sources/use-post-resource-value-source.ts index 8d9fd94edd6..62e77f5dcff 100644 --- a/apps/posts/src/hooks/filter-sources/use-post-resource-value-source.ts +++ b/apps/posts/src/hooks/filter-sources/use-post-resource-value-source.ts @@ -68,7 +68,11 @@ const usePublishedPageValueSource = createGhostBrowseValueSource ({ + value, + label: `ID: ${value}` + }) ); export function usePostResourceValueSource(): ValueSource { diff --git a/apps/posts/src/views/comments/comment-fields.ts b/apps/posts/src/views/comments/comment-fields.ts new file mode 100644 index 00000000000..e9966fa1598 --- /dev/null +++ b/apps/posts/src/views/comments/comment-fields.ts @@ -0,0 +1,129 @@ +import {DATE_FILTER_OPERATORS, DEFAULT_DATE_OPERATOR} from '../filters/filter-date'; +import {dateCodec, scalarCodec, textCodec} from '../filters/filter-codecs'; +import {defineFields} from '../filters/filter-types'; +import {extractComparator} from '../filters/filter-ast'; +import type {FilterCodec} from '../filters/filter-types'; + +const reportedCodec: FilterCodec = { + parse(node, ctx) { + const comparator = extractComparator(node as Record); + + if (!comparator || comparator.field !== 'count.reports') { + return null; + } + + if (comparator.operator === '$eq' && comparator.value === 0) { + return { + field: ctx.key, + operator: 'is', + values: ['false'] + }; + } + + if (comparator.operator === '$gt' && comparator.value === 0) { + return { + field: ctx.key, + operator: 'is', + values: ['true'] + }; + } + + return null; + }, + serialize(predicate) { + const value = predicate.values[0]; + + if (predicate.operator !== 'is') { + return null; + } + + if (value === 'true') { + return ['count.reports:>0']; + } + + if (value === 'false') { + return ['count.reports:0']; + } + + return null; + } +}; + +export const commentFields = defineFields({ + status: { + operators: ['is'], + ui: { + label: 'Status', + type: 'select', + searchable: false, + hideOperatorSelect: true + }, + options: [ + {value: 'published', label: 'Published'}, + {value: 'hidden', label: 'Hidden'} + ], + codec: scalarCodec() + }, + created_at: { + operators: DATE_FILTER_OPERATORS, + ui: { + label: 'Date', + defaultOperator: DEFAULT_DATE_OPERATOR, + type: 'date', + className: 'w-full max-w-32' + }, + codec: dateCodec() + }, + body: { + operators: ['contains', 'does-not-contain'], + parseKeys: ['html'], + ui: { + label: 'Text', + type: 'text', + placeholder: 'Search comment text...', + defaultOperator: 'contains', + className: 'w-full max-w-48', + popoverContentClassName: 'w-full max-w-48' + }, + codec: textCodec({field: 'html'}) + }, + post: { + operators: ['is', 'is-not'], + parseKeys: ['post_id'], + ui: { + label: 'Post', + type: 'select', + searchable: true, + className: 'w-full max-w-80', + popoverContentClassName: 'w-full max-w-[calc(100vw-32px)] max-w-80' + }, + codec: scalarCodec({field: 'post_id'}) + }, + author: { + operators: ['is', 'is-not'], + parseKeys: ['member_id'], + ui: { + label: 'Author', + type: 'select', + searchable: true, + className: 'w-80', + popoverContentClassName: 'w-80' + }, + codec: scalarCodec({field: 'member_id'}) + }, + reported: { + operators: ['is'], + parseKeys: ['count.reports'], + ui: { + label: 'Reported', + type: 'select', + searchable: false, + hideOperatorSelect: true + }, + options: [ + {value: 'true', label: 'Yes'}, + {value: 'false', label: 'No'} + ], + codec: reportedCodec + } +}); diff --git a/apps/posts/src/views/comments/comment-filter-query.ts b/apps/posts/src/views/comments/comment-filter-query.ts new file mode 100644 index 00000000000..88fc2d998ef --- /dev/null +++ b/apps/posts/src/views/comments/comment-filter-query.ts @@ -0,0 +1,38 @@ +import {commentFields} from './comment-fields'; +import {dispatchSimpleNodes, getFieldKeysByType, hasFieldKey, parseFilterToAst, serializePredicates, stampPredicates} from '../filters/filter-query-core'; +import type {AstNode} from '../filters/filter-ast'; +import type {FilterPredicate, ParsedPredicate} from '../filters/filter-types'; + +const TIMEZONE_SENSITIVE_COMMENT_FIELDS = getFieldKeysByType(commentFields, 'date'); + +function parseCommentNode(node: AstNode, timezone: string): ParsedPredicate[] { + if (Array.isArray(node.$and)) { + return (node.$and as AstNode[]).flatMap(child => parseCommentNode(child, timezone)); + } + + return dispatchSimpleNodes([node], commentFields, timezone); +} + +export function parseCommentFilter(filter: string | undefined, timezone: string): FilterPredicate[] { + const ast = parseFilterToAst(filter ?? ''); + + if (!ast) { + return []; + } + + return stampPredicates(parseCommentNode(ast, timezone)); +} + +export function hasTimezoneSensitiveCommentFilter(filter: string | undefined): boolean { + const ast = parseFilterToAst(filter ?? ''); + + if (!ast) { + return false; + } + + return hasFieldKey(ast, TIMEZONE_SENSITIVE_COMMENT_FIELDS); +} + +export function serializeCommentFilters(predicates: FilterPredicate[], timezone: string): string | undefined { + return serializePredicates(predicates, commentFields, timezone); +} diff --git a/apps/posts/src/views/comments/comments.tsx b/apps/posts/src/views/comments/comments.tsx index 8b667eb2341..6048bb528f2 100644 --- a/apps/posts/src/views/comments/comments.tsx +++ b/apps/posts/src/views/comments/comments.tsx @@ -3,23 +3,65 @@ import CommentsFilters from './components/comments-filters'; import CommentsHeader from './components/comments-header'; import CommentsLayout from './components/comments-layout'; import CommentsList from './components/comments-list'; -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {Button, EmptyIndicator, LoadingIndicator} from '@tryghost/shade/components'; import {LucideIcon} from '@tryghost/shade/utils'; import {createFilter} from '@tryghost/shade/patterns'; +import {escapeNqlString} from '../filters/filter-normalization'; +import {getSiteTimezone} from '@src/utils/get-site-timezone'; +import {serializeCommentFilters} from './comment-filter-query'; +import {shouldDelayCommentDateFilterHydration, useFilterState} from './hooks/use-filter-state'; import {useBrowseComments} from '@tryghost/admin-x-framework/api/comments'; -import {useFilterState} from './hooks/use-filter-state'; +import {useBrowseSettings} from '@tryghost/admin-x-framework/api/settings'; +import {useSearchParams} from 'react-router'; -const Comments: React.FC = () => { - const {filters, nql, setFilters, clearFilters, isSingleIdFilter} = useFilterState(); +function getSingleCommentIdParam(searchParams: URLSearchParams): string | undefined { + const value = searchParams.get('id'); + const match = value?.match(/^is:(.+)$/); + + return match?.[1]; +} + +const CommentsPage: React.FC<{timezone: string; singleCommentId?: string}> = ({ + timezone, + singleCommentId +}) => { + const [searchParams, setSearchParams] = useSearchParams(); + const {filters, nql, setFilters} = useFilterState(timezone); const handleAddFilter = useCallback((field: string, value: string, operator: string = 'is') => { - setFilters((prevFilters) => { - // Remove any existing filter for the same field - const filtered = prevFilters.filter(f => f.field !== field); - // Add the new filter - return [...filtered, createFilter(field, operator, [value])]; - }, {replace: false}); - }, [setFilters]); + const nextFilters = [ + ...filters.filter(filter => filter.field !== field), + createFilter(field, operator, [value]) + ]; + + if (!singleCommentId) { + setFilters(nextFilters, {replace: false}); + return; + } + + const nextSearchParams = new URLSearchParams(searchParams); + const nextNql = serializeCommentFilters(nextFilters, timezone); + + nextSearchParams.delete('id'); + nextSearchParams.delete('filter'); + + if (nextNql) { + nextSearchParams.set('filter', nextNql); + } + + setSearchParams(nextSearchParams, {replace: false}); + }, [filters, searchParams, setFilters, setSearchParams, singleCommentId, timezone]); + const effectiveFilter = useMemo(() => { + if (singleCommentId) { + return `id:${escapeNqlString(singleCommentId)}`; + } + + return nql; + }, [nql, singleCommentId]); + + const handleShowAllComments = useCallback(() => { + setSearchParams(new URLSearchParams(), {replace: false}); + }, [setSearchParams]); const { data, @@ -30,18 +72,21 @@ const Comments: React.FC = () => { fetchNextPage, hasNextPage } = useBrowseComments({ - searchParams: nql ? {filter: nql} : {}, + searchParams: { + ...(effectiveFilter ? {filter: effectiveFilter} : {}) + }, keepPreviousData: true }); - // If we are fetching comments, but not fetching the next page and not refetching, we should show the loading indicator const shouldShowLoading = isFetching && !isFetchingNextPage && !isRefetching; + const resetKey = effectiveFilter ?? ''; return ( - {!isSingleIdFilter && ( + {!singleCommentId && ( )} @@ -65,11 +110,22 @@ const Comments: React.FC = () => { ) : !data?.comments.length ? (
- - - + {singleCommentId ? ( +
+ + + + +
+ ) : ( + + + + )}
) : ( <> @@ -79,13 +135,13 @@ const Comments: React.FC = () => { isFetchingNextPage={isFetchingNextPage} isLoading={isFetching && !isFetchingNextPage} items={data?.comments ?? []} - resetKey={nql ?? ''} + resetKey={resetKey} totalItems={data?.meta?.pagination?.total ?? 0} onAddFilter={handleAddFilter} /> - {isSingleIdFilter && ( + {singleCommentId && (
-
@@ -97,4 +153,29 @@ const Comments: React.FC = () => { ); }; +const Comments: React.FC = () => { + const [searchParams] = useSearchParams(); + const {data: settingsData, isLoading: isSettingsLoading} = useBrowseSettings({}); + const singleCommentId = useMemo(() => getSingleCommentIdParam(searchParams), [searchParams]); + const filterParam = searchParams.get('filter') ?? undefined; + const shouldDelayHydration = !singleCommentId && shouldDelayCommentDateFilterHydration(filterParam, Boolean(settingsData), isSettingsLoading); + + if (shouldDelayHydration) { + return ( + + + +
+ +
+
+
+ ); + } + + const timezone = getSiteTimezone(settingsData?.settings ?? []); + + return ; +}; + export default Comments; diff --git a/apps/posts/src/views/comments/components/comments-filters.tsx b/apps/posts/src/views/comments/components/comments-filters.tsx index 5f689f44364..1b59dc17f59 100644 --- a/apps/posts/src/views/comments/components/comments-filters.tsx +++ b/apps/posts/src/views/comments/components/comments-filters.tsx @@ -1,110 +1,28 @@ -import React, {useMemo} from 'react'; -import {Filter, FilterFieldConfig, Filters} from '@tryghost/shade/patterns'; +import React from 'react'; +import {Filter, Filters} from '@tryghost/shade/patterns'; import {LucideIcon, cn} from '@tryghost/shade/utils'; +import {useCommentFilterFields} from '../use-comment-filter-fields'; import {useMemberValueSource} from '@src/hooks/filter-sources/use-member-value-source'; import {usePostResourceValueSource} from '@src/hooks/filter-sources/use-post-resource-value-source'; interface CommentsFiltersProps { filters: Filter[]; + siteTimezone: string; onFiltersChange: (filters: Filter[]) => void; } const CommentsFilters: React.FC = ({ filters, + siteTimezone, onFiltersChange }) => { const postValueSource = usePostResourceValueSource(); const memberValueSource = useMemberValueSource(); - - const filterFields: FilterFieldConfig[] = useMemo( - () => [ - { - key: 'author', - label: 'Author', - type: 'select', - icon: , - searchable: true, - valueSource: memberValueSource, - className: 'w-80', - popoverContentClassName: 'w-80', - operators: [ - {value: 'is', label: 'is'}, - {value: 'is_not', label: 'is not'} - ] - }, - { - key: 'post', - label: 'Post', - type: 'select', - icon: , - searchable: true, - valueSource: postValueSource, - className: 'w-full max-w-80', - popoverContentClassName: 'w-full max-w-[calc(100vw-32px)] max-w-80', - operators: [ - {value: 'is', label: 'is'}, - {value: 'is_not', label: 'is not'} - ] - }, - { - key: 'body', - label: 'Text', - type: 'text', - icon: , - placeholder: 'Search comment text...', - operators: [ - {value: 'contains', label: 'contains'}, - {value: 'not_contains', label: 'does not contain'} - ], - defaultOperator: 'contains', - className: 'w-full max-w-48', - popoverContentClassName: 'w-full max-w-48' - }, - { - key: 'status', - label: 'Status', - type: 'select', - icon: , - options: [ - {value: 'published', label: 'Published'}, - {value: 'hidden', label: 'Hidden'} - ], - operators: [ - {value: 'is', label: 'is'} - ], - searchable: false, - hideOperatorSelect: true - }, - { - key: 'reported', - label: 'Reported', - type: 'select', - icon: , - options: [ - {value: 'true', label: 'Yes'}, - {value: 'false', label: 'No'} - ], - operators: [ - {value: 'is', label: 'is'} - ], - searchable: false, - hideOperatorSelect: true - }, - { - key: 'created_at', - label: 'Date', - type: 'date', - className: 'w-full max-w-32', - icon: , - operators: [ - {value: 'is', label: 'is'}, - {value: 'before', label: 'before'}, - {value: 'after', label: 'after'} - ] - } - ], - [memberValueSource, postValueSource] - ); + const filterFields = useCommentFilterFields({ + memberValueSource, + postValueSource, + siteTimezone + }); const hasFilters = filters.length > 0; diff --git a/apps/posts/src/views/comments/hooks/use-filter-state.ts b/apps/posts/src/views/comments/hooks/use-filter-state.ts index 0e6822745b3..e771dfb7ee2 100644 --- a/apps/posts/src/views/comments/hooks/use-filter-state.ts +++ b/apps/posts/src/views/comments/hooks/use-filter-state.ts @@ -1,213 +1,105 @@ import {Filter} from '@tryghost/shade/patterns'; -import {useCallback, useMemo} from 'react'; +import {hasTimezoneSensitiveCommentFilter, parseCommentFilter, serializeCommentFilters} from '../comment-filter-query'; +import {parseLegacyCommentFilters, removeLegacyCommentFilterParams} from '../legacy-comment-filter-query'; +import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {useSearchParams} from '@tryghost/admin-x-framework'; -/** - * Comment filter field keys - single source of truth for filter definitions - */ -export const COMMENT_FILTER_FIELDS = ['id', 'status', 'created_at', 'body', 'post', 'author', 'reported'] as const; - -export type CommentFilterField = typeof COMMENT_FILTER_FIELDS[number]; - -export function buildNqlFilter(filters: Filter[]): string | undefined { - const parts: string[] = []; - - for (const filter of filters) { - if (!filter.values[0]) { - continue; - } - - switch (filter.field) { - case 'id': - parts.push(`id:'${filter.values[0]}'`); - break; - - case 'status': - parts.push(`status:${filter.values[0]}`); - break; - - case 'created_at': - if (filter.operator === 'before' && filter.values[0]) { - parts.push(`created_at:<'${filter.values[0]}'`); - } else if (filter.operator === 'after' && filter.values[0]) { - parts.push(`created_at:>'${filter.values[0]}'`); - } else if (filter.operator === 'is' && filter.values[0]) { - // Match all items from the selected day in the user's timezone - const dateValue = String(filter.values[0]); // Format: YYYY-MM-DD - - // Create Date objects in user's local timezone, then convert to UTC - const startOfDay = new Date(dateValue + 'T00:00:00').toISOString(); - const endOfDay = new Date(dateValue + 'T23:59:59.999').toISOString(); - - parts.push(`created_at:>='${startOfDay}'+created_at:<='${endOfDay}'`); - } - break; - - case 'body': - const value = filter.values[0] as string; - // Escape single quotes in the value - const escapedValue = value.replace(/'/g, '\\\''); - - if (filter.operator === 'contains') { - parts.push(`html:~'${escapedValue}'`); - } else if (filter.operator === 'not_contains') { - parts.push(`html:-~'${escapedValue}'`); - } - break; - - case 'post': - if (filter.operator === 'is_not') { - parts.push(`post_id:-${filter.values[0]}`); - } else { - // Default to 'is' operator - parts.push(`post_id:${filter.values[0]}`); - } - break; - - case 'author': - if (filter.operator === 'is_not') { - parts.push(`member_id:-${filter.values[0]}`); - } else { - // Default to 'is' operator - parts.push(`member_id:${filter.values[0]}`); - } - break; - - case 'reported': - if (filter.values[0] === 'true') { - parts.push('count.reports:>0'); - } else if (filter.values[0] === 'false') { - parts.push('count.reports:0'); - } - break; - } - } +type SetFiltersAction = Filter[] | ((prevFilters: Filter[]) => Filter[]); - return parts.length ? parts.join('+') : undefined; +interface SetFiltersOptions { + /** Whether to replace the current history entry (default: true) */ + replace?: boolean; } -/** - * Parse a filter value from URL format: "operator:value" - * e.g., "is:published", "contains:hello" - */ -function parseFilterValue(queryValue: string): {operator: string; value: string} | null { - if (!queryValue) { - return null; - } - const colonIndex = queryValue.indexOf(':'); - if (colonIndex <= 0) { - return null; // Invalid format, must have operator:value - } - - return { - operator: queryValue.substring(0, colonIndex), - value: queryValue.substring(colonIndex + 1) - }; +interface UseFilterStateReturn { + filters: Filter[]; + nql: string | undefined; + setFilters: (action: SetFiltersAction, options?: SetFiltersOptions) => void; + clearFilters: (options?: SetFiltersOptions) => void; } -/** - * Parse URL search params into Filter objects - * Preserves the order of filters as they appear in the URL - */ -function searchParamsToFilters(searchParams: URLSearchParams): Filter[] { - const filters: Filter[] = []; - - // Iterate over URL params in order to preserve filter order - for (const [field, queryValue] of searchParams.entries()) { - // Only process valid filter fields - if (!COMMENT_FILTER_FIELDS.includes(field as CommentFilterField)) { - continue; - } +function toSearchParams(baseSearchParams: URLSearchParams, filters: Filter[], timezone: string): URLSearchParams { + const params = new URLSearchParams(baseSearchParams); + const filter = serializeCommentFilters(filters, timezone); - if (!queryValue) { - continue; - } + params.delete('filter'); + removeLegacyCommentFilterParams(params); - const parsed = parseFilterValue(queryValue); - if (parsed) { - filters.push({ - id: field, - field, - operator: parsed.operator, - values: [parsed.value] - }); - } + if (filter) { + params.set('filter', filter); } - return filters; + return params; } -/** - * Serialize filters to URL search params format - */ -function filtersToSearchParams(filters: Filter[]): URLSearchParams { - const params = new URLSearchParams(); +export function shouldDelayCommentDateFilterHydration( + filterParam: string | undefined, + hasResolvedTimezone: boolean, + isSettingsLoading: boolean = !hasResolvedTimezone +): boolean { + return Boolean(filterParam) && isSettingsLoading && !hasResolvedTimezone && hasTimezoneSensitiveCommentFilter(filterParam); +} - for (const filter of filters) { - if (COMMENT_FILTER_FIELDS.includes(filter.field as CommentFilterField) && filter.values[0] !== undefined) { - const value = `${filter.operator}:${String(filter.values[0])}`; - params.set(filter.field, value); - } - } +export function useFilterState(timezone: string): UseFilterStateReturn { + const [searchParams, setSearchParams] = useSearchParams(); + const lastWrittenQueryRef = useRef(null); + const filterParam = useMemo(() => searchParams.get('filter') ?? undefined, [searchParams]); + const currentQuery = useMemo(() => searchParams.toString(), [searchParams]); - return params; -} + const parsedFilters = useMemo(() => { + if (filterParam !== undefined) { + return parseCommentFilter(filterParam, timezone); + } -type SetFiltersAction = Filter[] | ((prevFilters: Filter[]) => Filter[]); + return parseLegacyCommentFilters(searchParams); + }, [filterParam, searchParams, timezone]); + const [filters, setDraftFilters] = useState(parsedFilters); -interface SetFiltersOptions { - /** Whether to replace the current history entry (default: true) */ - replace?: boolean; -} + const nql = useMemo(() => { + return serializeCommentFilters(filters, timezone); + }, [filters, timezone]); -interface ClearFiltersOptions { - /** Whether to replace the current history entry (default: true) */ - replace?: boolean; -} + useEffect(() => { + if (currentQuery !== lastWrittenQueryRef.current) { + setDraftFilters(parsedFilters); + lastWrittenQueryRef.current = currentQuery; + } + }, [currentQuery, parsedFilters]); -interface UseFilterStateReturn { - filters: Filter[]; - nql: string | undefined; - setFilters: (action: SetFiltersAction, options?: SetFiltersOptions) => void; - clearFilters: (options?: ClearFiltersOptions) => void; - /** True when the only active filter is a single comment ID (used for deep linking) */ - isSingleIdFilter: boolean; -} + useEffect(() => { + if (lastWrittenQueryRef.current !== null && currentQuery !== lastWrittenQueryRef.current) { + return; + } -/** - * Hook to sync comment filter state with URL query parameters - * - * URL format: ?status=is:published&author=is:member-id&body=contains:search+term - */ -export function useFilterState(): UseFilterStateReturn { - const [searchParams, setSearchParams] = useSearchParams(); + const nextParams = toSearchParams(searchParams, filters, timezone); + const nextQuery = nextParams.toString(); - // Parse filters from URL - const filters = useMemo(() => { - return searchParamsToFilters(searchParams); - }, [searchParams]); + if (nextQuery !== currentQuery) { + lastWrittenQueryRef.current = nextQuery; + setSearchParams(nextParams, {replace: true}); + } + }, [currentQuery, filters, searchParams, setSearchParams, timezone]); - // Update URL when filters change const setFilters = useCallback((action: SetFiltersAction, options: SetFiltersOptions = {}) => { const newFilters = typeof action === 'function' ? action(filters) : action; - const newParams = filtersToSearchParams(newFilters); - - // Update URL - replace by default, but allow pushing to history + const newParams = toSearchParams(searchParams, newFilters, timezone); const replace = options.replace ?? true; + + setDraftFilters(newFilters); + lastWrittenQueryRef.current = newParams.toString(); setSearchParams(newParams, {replace}); - }, [filters, setSearchParams]); + }, [filters, searchParams, setSearchParams, timezone]); - // Clear all filter params from URL - const clearFilters = useCallback(({replace = true}: {replace?: boolean} = {}) => { - setSearchParams(new URLSearchParams(), {replace}); - }, [setSearchParams]); + const clearFilters = useCallback(({replace = true}: SetFiltersOptions = {}) => { + const newParams = new URLSearchParams(searchParams); - const nql = useMemo(() => buildNqlFilter(filters), [filters]); + newParams.delete('filter'); - // Check if the only active filter is a single comment ID (used for deep linking) - const isSingleIdFilter = useMemo(() => { - return filters.length === 1 && filters[0].field === 'id'; - }, [filters]); + removeLegacyCommentFilterParams(newParams); + setDraftFilters([]); + lastWrittenQueryRef.current = newParams.toString(); + setSearchParams(newParams, {replace}); + }, [searchParams, setSearchParams]); - return {filters, nql, setFilters, clearFilters, isSingleIdFilter}; + return {filters, nql, setFilters, clearFilters}; } diff --git a/apps/posts/src/views/comments/legacy-comment-filter-query.ts b/apps/posts/src/views/comments/legacy-comment-filter-query.ts new file mode 100644 index 00000000000..8c40b642c60 --- /dev/null +++ b/apps/posts/src/views/comments/legacy-comment-filter-query.ts @@ -0,0 +1,61 @@ +import {Filter} from '@tryghost/shade/patterns'; + +// TODO: Remove this file after the comment filters migration has safely rolled out. +const LEGACY_COMMENT_FILTER_FIELDS = ['status', 'created_at', 'body', 'post', 'author', 'reported'] as const; +const LEGACY_OPERATOR_MAP: Record = { + is_not: 'is-not', + not_contains: 'does-not-contain', + before: 'is-less', + after: 'is-greater', + on_or_before: 'is-or-less', + on_or_after: 'is-or-greater' +}; + +function parseLegacyFilterValue(queryValue: string): {operator: string; value: string} | null { + const colonIndex = queryValue.indexOf(':'); + + if (colonIndex <= 0) { + return null; + } + + const operator = queryValue.substring(0, colonIndex); + const value = queryValue.substring(colonIndex + 1); + + if (!value) { + return null; + } + + return { + operator: LEGACY_OPERATOR_MAP[operator] ?? operator, + value + }; +} + +export function parseLegacyCommentFilters(searchParams: URLSearchParams): Filter[] { + const filters: Filter[] = []; + + for (const [field, queryValue] of searchParams.entries()) { + if (!LEGACY_COMMENT_FILTER_FIELDS.includes(field as typeof LEGACY_COMMENT_FILTER_FIELDS[number])) { + continue; + } + + const parsed = parseLegacyFilterValue(queryValue); + + if (!parsed) { + continue; + } + + filters.push({ + id: `${field}:${filters.length + 1}`, + field, + operator: parsed.operator, + values: [parsed.value] + }); + } + + return filters; +} + +export function removeLegacyCommentFilterParams(searchParams: URLSearchParams): void { + LEGACY_COMMENT_FILTER_FIELDS.forEach(field => searchParams.delete(field)); +} diff --git a/apps/posts/src/views/comments/use-comment-filter-fields.ts b/apps/posts/src/views/comments/use-comment-filter-fields.ts new file mode 100644 index 00000000000..f6cb8b92200 --- /dev/null +++ b/apps/posts/src/views/comments/use-comment-filter-fields.ts @@ -0,0 +1,59 @@ +import React, {useMemo} from 'react'; +import {DATE_OPERATOR_LABELS} from '../filters/filter-date'; +import {FilterFieldConfig, ValueSource} from '@tryghost/shade/patterns'; +import {LucideIcon} from '@tryghost/shade/utils'; +import {commentFields} from './comment-fields'; +import {createOperatorOptions} from '../filters/filter-operator-options'; +import {getTodayInTimezone} from '../filters/filter-normalization'; + +interface UseCommentFilterFieldsOptions { + postValueSource: ValueSource; + memberValueSource: ValueSource; + siteTimezone?: string; +} + +const COMMENT_FIELD_ORDER = ['author', 'post', 'body', 'status', 'reported', 'created_at'] as const; + +function getFieldIcon(key: string) { + switch (key) { + case 'author': + return React.createElement(LucideIcon.User, {className: 'size-4'}); + case 'post': + return React.createElement(LucideIcon.FileText, {className: 'size-4'}); + case 'body': + return React.createElement(LucideIcon.MessageSquareText, {className: 'size-4'}); + case 'status': + return React.createElement(LucideIcon.Circle, {className: 'size-4'}); + case 'reported': + return React.createElement(LucideIcon.Flag, {className: 'size-4'}); + case 'created_at': + return React.createElement(LucideIcon.Calendar, {className: 'size-4'}); + default: + return undefined; + } +} + +export function useCommentFilterFields({ + postValueSource, + memberValueSource, + siteTimezone = 'UTC' +}: UseCommentFilterFieldsOptions): FilterFieldConfig[] { + return useMemo(() => { + const today = getTodayInTimezone(siteTimezone); + + return COMMENT_FIELD_ORDER.map((key) => { + const field = commentFields[key]; + + return { + key, + ...field.ui, + icon: getFieldIcon(key), + operators: createOperatorOptions(field.operators, {labels: DATE_OPERATOR_LABELS}), + ...('options' in field && field.options ? {options: field.options} : {}), + ...(key === 'created_at' ? {defaultValue: today} : {}), + ...(key === 'author' ? {valueSource: memberValueSource} : {}), + ...(key === 'post' ? {valueSource: postValueSource} : {}) + }; + }); + }, [memberValueSource, postValueSource, siteTimezone]); +} diff --git a/apps/posts/src/views/filters/filter-codecs.test.ts b/apps/posts/src/views/filters/filter-codecs.test.ts index ba805292c4e..dc27f8ba39d 100644 --- a/apps/posts/src/views/filters/filter-codecs.test.ts +++ b/apps/posts/src/views/filters/filter-codecs.test.ts @@ -1,6 +1,6 @@ import nql from '@tryghost/nql-lang'; +import {dateCodec, numberCodec, scalarCodec, setCodec, textCodec} from './filter-codecs'; import {describe, expect, it} from 'vitest'; -import {numberCodec, scalarCodec, setCodec, textCodec} from './filter-codecs'; import type {CodecContext, FilterPredicate} from './filter-types'; const statusContext: CodecContext = { @@ -52,6 +52,13 @@ const countContext: CodecContext = { timezone: 'UTC' }; +const dateContext: CodecContext = { + key: 'created_at', + pattern: 'created_at', + params: {}, + timezone: 'UTC' +}; + describe('scalarCodec', () => { it('parses simple scalar comparisons', () => { expect(scalarCodec().parse(nql.parse('status:paid') as never, statusContext)).toEqual({ @@ -335,3 +342,41 @@ describe('numberCodec', () => { expect(numberCodec().serialize(predicate, countContext)).toEqual(['email_count:<=10']); }); }); + +describe('dateCodec', () => { + it('parses date comparison operators', () => { + expect(dateCodec().parse(nql.parse('created_at:<=\'2024-01-01T23:59:59.999Z\'') as never, dateContext)).toEqual({ + field: 'created_at', + operator: 'is-or-less', + values: ['2024-01-01'] + }); + + expect(dateCodec().parse(nql.parse('created_at:>\'2024-01-01T23:59:59.999Z\'') as never, dateContext)).toEqual({ + field: 'created_at', + operator: 'is-greater', + values: ['2024-01-01'] + }); + }); + + it('serializes date comparison operators using site timezone day bounds', () => { + expect(dateCodec().serialize({ + id: '1', + field: 'created_at', + operator: 'is-or-less', + values: ['2024-02-01'] + }, { + ...dateContext, + timezone: 'Europe/Stockholm' + })).toEqual(['created_at:<=\'2024-02-01T22:59:59.999Z\'']); + }); + + it('returns null for invalid date values', () => { + expect(dateCodec().parse(nql.parse('created_at:<=\'not-a-date\'') as never, dateContext)).toBeNull(); + expect(dateCodec().serialize({ + id: '1', + field: 'created_at', + operator: 'is-or-less', + values: ['not-a-date'] + }, dateContext)).toBeNull(); + }); +}); diff --git a/apps/posts/src/views/filters/filter-codecs.ts b/apps/posts/src/views/filters/filter-codecs.ts index 5c6f4bbd525..06d8c4662ca 100644 --- a/apps/posts/src/views/filters/filter-codecs.ts +++ b/apps/posts/src/views/filters/filter-codecs.ts @@ -1,7 +1,10 @@ -import {escapeNqlString} from './filter-normalization'; +import {DATE_FILTER_OPERATORS} from './filter-date'; +import {escapeNqlString, formatDateInTimezone, getDayBoundsInUtc} from './filter-normalization'; import {extractComparator} from './filter-ast'; import type {FilterCodec} from './filter-types'; +type DateOperator = typeof DATE_FILTER_OPERATORS[number]; + const SCALAR_OPERATORS: Record = { $eq: 'is', $ne: 'is-not' @@ -15,6 +18,13 @@ const NUMBER_OPERATORS: Record = { $lte: 'is-or-less' }; +const DATE_OPERATORS: Record = { + $lt: 'is-less', + $lte: 'is-or-less', + $gt: 'is-greater', + $gte: 'is-or-greater' +}; + const TEXT_OPERATOR_SYMBOLS: Record = { contains: '~', 'does-not-contain': '-~', @@ -32,6 +42,13 @@ const NUMBER_OPERATOR_SYMBOLS: Record = { 'is-or-less': '<=' }; +const DATE_OPERATOR_SYMBOLS: Record = { + 'is-less': '<', + 'is-or-less': '<=', + 'is-greater': '>', + 'is-or-greater': '>=' +}; + const SET_OPERATOR_SYMBOLS: Record = { 'is-any': '', 'is-not-any': '-' @@ -314,3 +331,56 @@ export function numberCodec(config?: CodecConfig): FilterCodec { } }; } + +export function dateCodec(config?: CodecConfig): FilterCodec { + return { + parse(node, ctx) { + const comparator = extractComparator(node as Record); + const field = getCodecField(config, ctx.key); + + if (!comparator || comparator.field !== field || typeof comparator.value !== 'string') { + return null; + } + + const operator = DATE_OPERATORS[comparator.operator]; + const value = formatDateInTimezone(comparator.value, ctx.timezone); + + if (!operator || !value) { + return null; + } + + return { + field: ctx.key, + operator, + values: [value] + }; + }, + serialize(predicate, ctx) { + const rawValue = predicate.values[0]; + const field = getCodecField(config, ctx.key); + + if (typeof rawValue !== 'string' || rawValue === '') { + return null; + } + + const value = formatDateInTimezone(rawValue, ctx.timezone); + + if (!value) { + return null; + } + + const {start, end} = getDayBoundsInUtc(value, ctx.timezone); + const operator = DATE_OPERATOR_SYMBOLS[predicate.operator]; + + if (operator === undefined) { + return null; + } + + const boundary = predicate.operator === 'is-less' || predicate.operator === 'is-or-greater' + ? start + : end; + + return [`${field}:${operator}'${boundary}'`]; + } + }; +} diff --git a/apps/posts/src/views/filters/filter-date.ts b/apps/posts/src/views/filters/filter-date.ts new file mode 100644 index 00000000000..0bdd1567e43 --- /dev/null +++ b/apps/posts/src/views/filters/filter-date.ts @@ -0,0 +1,10 @@ +export const DATE_FILTER_OPERATORS = ['is-less', 'is-or-less', 'is-greater', 'is-or-greater'] as const; + +export const DATE_OPERATOR_LABELS: Record = { + 'is-less': 'before', + 'is-or-less': 'on or before', + 'is-greater': 'after', + 'is-or-greater': 'on or after' +}; + +export const DEFAULT_DATE_OPERATOR = 'is-or-less'; diff --git a/apps/posts/src/views/filters/filter-normalization.test.ts b/apps/posts/src/views/filters/filter-normalization.test.ts deleted file mode 100644 index 22cd601bacc..00000000000 --- a/apps/posts/src/views/filters/filter-normalization.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {describe, expect, it} from 'vitest'; -import {escapeNqlString} from './filter-normalization'; - -describe('filter-normalization', () => { - it('escapes single quotes for NQL strings', () => { - expect(escapeNqlString('can\'t stop')).toBe('\'can\\\'t stop\''); - }); - - it('escapes backslashes before single quotes for NQL strings', () => { - expect(escapeNqlString('test\\\'value')).toBe('\'test\\\\\\\'value\''); - }); -}); diff --git a/apps/posts/src/views/filters/filter-normalization.ts b/apps/posts/src/views/filters/filter-normalization.ts index 25fded8d67f..893b04c78db 100644 --- a/apps/posts/src/views/filters/filter-normalization.ts +++ b/apps/posts/src/views/filters/filter-normalization.ts @@ -1,3 +1,50 @@ +import {Temporal} from 'temporal-polyfill'; + export function escapeNqlString(value: string): string { return `'${value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}'`; } + +const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/; +const LEGACY_UTC_DATE_TIME_PATTERN = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/; + +export function formatDateInTimezone(value: string, timezone: string): string | null { + try { + if (DATE_ONLY_PATTERN.test(value)) { + return Temporal.PlainDate.from(value).toString(); + } + + const instantValue = LEGACY_UTC_DATE_TIME_PATTERN.test(value) + ? `${value.replace(' ', 'T')}Z` + : value; + + return Temporal.Instant.from(instantValue).toZonedDateTimeISO(timezone).toPlainDate().toString(); + } catch { + return null; + } +} + +export function getTodayInTimezone(timezone: string): string { + return Temporal.Now.zonedDateTimeISO(timezone).toPlainDate().toString(); +} + +export function getDayBoundsInUtc(date: string, timezone: string): {start: string; end: string} { + let plainDate: Temporal.PlainDate; + + try { + plainDate = Temporal.PlainDate.from(date); + } catch { + throw new Error(`Invalid filter date: ${date}`); + } + + try { + const start = plainDate.toPlainDateTime(Temporal.PlainTime.from('00:00:00')).toZonedDateTime(timezone).toInstant(); + const end = plainDate.toPlainDateTime(Temporal.PlainTime.from('23:59:59.999')).toZonedDateTime(timezone).toInstant(); + + return { + start: start.toString({fractionalSecondDigits: 3}), + end: end.toString({fractionalSecondDigits: 3}) + }; + } catch { + throw new Error(`Invalid timezone: ${timezone}`); + } +} diff --git a/apps/posts/src/views/filters/filter-operator-options.ts b/apps/posts/src/views/filters/filter-operator-options.ts new file mode 100644 index 00000000000..095f6a05bb0 --- /dev/null +++ b/apps/posts/src/views/filters/filter-operator-options.ts @@ -0,0 +1,20 @@ +interface OperatorOption { + value: string; + label: string; +} + +interface CreateOperatorOptionsOptions { + labels?: Record; +} + +export function createOperatorOptions( + operators: readonly string[], + options: CreateOperatorOptionsOptions = {} +): OperatorOption[] { + const labels = options.labels || {}; + + return operators.map(operator => ({ + value: operator, + label: labels[operator] ?? operator.replaceAll('-', ' ') + })); +} diff --git a/apps/posts/src/views/filters/filter-query-core.test.ts b/apps/posts/src/views/filters/filter-query-core.test.ts index fe97afadb41..4b5648acb06 100644 --- a/apps/posts/src/views/filters/filter-query-core.test.ts +++ b/apps/posts/src/views/filters/filter-query-core.test.ts @@ -1,6 +1,6 @@ import {defineFields} from './filter-types'; import {describe, expect, it} from 'vitest'; -import {dispatchSimpleNodes, parseFilterToAst, serializePredicates} from './filter-query-core'; +import {dispatchSimpleNodes, getFieldKeysByType, hasFieldKey, parseFilterToAst, serializePredicates} from './filter-query-core'; import {numberCodec, scalarCodec} from './filter-codecs'; import type {AstNode} from './filter-ast'; import type {FilterPredicate} from './filter-types'; @@ -38,6 +38,15 @@ const fields = defineFields({ type: 'select' }, codec: scalarCodec({field: 'member_id'}) + }, + created_at: { + operators: ['is-or-less'], + parseKeys: ['created_at_utc'], + ui: { + label: 'Created', + type: 'date' + }, + codec: scalarCodec({field: 'created_at_utc'}) } }); @@ -102,4 +111,12 @@ describe('filter-query-core', () => { expect(serializePredicates(parsed, fields, 'UTC')).toBe('email_count:>5+status:paid'); }); + + it('finds fields by UI type and declared parse aliases in nested AST nodes', () => { + const ast = parseFilterToAst('(status:paid,created_at_utc:<\'2024-01-01T00:00:00.000Z\')') as AstNode; + const fieldKeys = getFieldKeysByType(fields, 'date'); + + expect([...fieldKeys]).toEqual(['created_at', 'created_at_utc']); + expect(hasFieldKey(ast, fieldKeys)).toBe(true); + }); }); diff --git a/apps/posts/src/views/filters/filter-query-core.ts b/apps/posts/src/views/filters/filter-query-core.ts index 1b7a14fa8f8..eff5ae88f95 100644 --- a/apps/posts/src/views/filters/filter-query-core.ts +++ b/apps/posts/src/views/filters/filter-query-core.ts @@ -22,6 +22,38 @@ export function stampPredicates(predicates: ParsedPredicate[]): FilterPredicate[ })); } +export function getFieldKeysByType>( + fields: TFields, + type: FilterField['ui']['type'] +): Set { + const keys = new Set(); + + Object.entries(fields).forEach(([key, definition]) => { + if (definition.ui.type !== type) { + return; + } + + keys.add(key); + definition.parseKeys?.forEach(parseKey => keys.add(parseKey)); + }); + + return keys; +} + +export function hasFieldKey(node: AstNode, fieldKeys: ReadonlySet): boolean { + if (Object.keys(node).some(key => fieldKeys.has(key))) { + return true; + } + + return Object.values(node).some((value) => { + if (Array.isArray(value)) { + return value.some(child => child !== null && typeof child === 'object' && hasFieldKey(child as AstNode, fieldKeys)); + } + + return value !== null && typeof value === 'object' && !(value instanceof RegExp) && hasFieldKey(value as AstNode, fieldKeys); + }); +} + export function dispatchSimpleNodes>(nodes: AstNode[], fields: TFields, timezone: string): ParsedPredicate[] { return nodes.flatMap((node) => { const keys = Object.keys(node); diff --git a/apps/posts/src/views/members/member-fields.test.ts b/apps/posts/src/views/members/member-fields.test.ts index 970bf66a57a..8e750198852 100644 --- a/apps/posts/src/views/members/member-fields.test.ts +++ b/apps/posts/src/views/members/member-fields.test.ts @@ -113,7 +113,7 @@ describe('memberFields', () => { }); }); -describe('memberDateCodec', () => { +describe('dateCodec', () => { it('serializes date boundaries in UTC day bounds', () => { const predicate: FilterPredicate = { id: '1', diff --git a/apps/posts/src/views/members/member-fields.ts b/apps/posts/src/views/members/member-fields.ts index f7ccdea8729..a1eef83a4ed 100644 --- a/apps/posts/src/views/members/member-fields.ts +++ b/apps/posts/src/views/members/member-fields.ts @@ -1,18 +1,10 @@ -import moment from 'moment-timezone'; +import {DATE_FILTER_OPERATORS, DEFAULT_DATE_OPERATOR} from '../filters/filter-date'; +import {dateCodec, numberCodec, scalarCodec, setCodec, textCodec} from '../filters/filter-codecs'; import {defineFields} from '../filters/filter-types'; import {escapeNqlString} from '../filters/filter-normalization'; -import {numberCodec, scalarCodec, setCodec, textCodec} from '../filters/filter-codecs'; import type {FilterCodec} from '../filters/filter-types'; -function getDayBoundsInUtc(date: string, timezone: string): {start: string; end: string} { - const start = moment.tz(date, 'YYYY-MM-DD', timezone).startOf('day').utc().toISOString(); - const end = moment.tz(date, 'YYYY-MM-DD', timezone).endOf('day').utc().toISOString(); - - return {start, end}; -} - const TEXT_OPERATORS = ['is', 'contains', 'does-not-contain', 'starts-with', 'ends-with'] as const; -const DATE_OPERATORS = ['is-less', 'is-or-less', 'is-greater', 'is-or-greater'] as const; const NUMBER_OPERATORS = ['is', 'is-greater', 'is-less'] as const; const SCALAR_OPERATORS = ['is', 'is-not'] as const; const SET_OPERATORS = ['is-any', 'is-not-any'] as const; @@ -26,78 +18,6 @@ const SUBSCRIPTION_STATUS_OPTIONS: Array<{value: string; label: string}> = [ {value: 'incomplete_expired', label: 'Incomplete - Expired'} ]; -function formatDateValue(value: unknown, timezone: string): string | null { - if (typeof value !== 'string' || !value) { - return null; - } - - const legacyUtc = moment.utc(value, ['YYYY-MM-DD HH:mm:ss.SSS', 'YYYY-MM-DD HH:mm:ss'], true); - - if (legacyUtc.isValid()) { - return legacyUtc.tz(timezone).format('YYYY-MM-DD'); - } - - const parsed = moment.tz(value, moment.ISO_8601, true, timezone); - - if (!parsed.isValid()) { - return null; - } - - return parsed.format('YYYY-MM-DD'); -} - -const memberDateCodec: FilterCodec = { - parse(node, ctx) { - const entry = Object.entries(node as Record)[0]; - - if (!entry || entry[0] !== ctx.key || typeof entry[1] !== 'object' || entry[1] === null) { - return null; - } - - const [operator, rawValue] = Object.entries(entry[1] as Record)[0] ?? []; - const value = formatDateValue(rawValue, ctx.timezone); - - if (!value) { - return null; - } - - switch (operator) { - case '$lt': - return {field: ctx.key, operator: 'is-less', values: [value]}; - case '$lte': - return {field: ctx.key, operator: 'is-or-less', values: [value]}; - case '$gt': - return {field: ctx.key, operator: 'is-greater', values: [value]}; - case '$gte': - return {field: ctx.key, operator: 'is-or-greater', values: [value]}; - default: - return null; - } - }, - serialize(predicate, ctx) { - const value = predicate.values[0]; - - if (typeof value !== 'string' || !value) { - return null; - } - - const {start, end} = getDayBoundsInUtc(value, ctx.timezone); - - switch (predicate.operator) { - case 'is-less': - return [`${ctx.key}:<'${start}'`]; - case 'is-or-less': - return [`${ctx.key}:<='${end}'`]; - case 'is-greater': - return [`${ctx.key}:>'${end}'`]; - case 'is-or-greater': - return [`${ctx.key}:>='${start}'`]; - default: - return null; - } - } -}; - const subscribedCodec: FilterCodec = { parse() { return null; @@ -226,24 +146,24 @@ export const memberFields = defineFields({ codec: subscribedCodec }, last_seen_at: { - operators: DATE_OPERATORS, + operators: DATE_FILTER_OPERATORS, ui: { label: 'Last seen', type: 'date', - defaultOperator: 'is-or-less', + defaultOperator: DEFAULT_DATE_OPERATOR, className: 'w-40' }, - codec: memberDateCodec + codec: dateCodec() }, created_at: { - operators: DATE_OPERATORS, + operators: DATE_FILTER_OPERATORS, ui: { label: 'Created', type: 'date', - defaultOperator: 'is-or-less', + defaultOperator: DEFAULT_DATE_OPERATOR, className: 'w-40' }, - codec: memberDateCodec + codec: dateCodec() }, signup: { operators: SCALAR_OPERATORS, @@ -340,11 +260,11 @@ export const memberFields = defineFields({ codec: scalarCodec() }, 'subscriptions.start_date': { - operators: DATE_OPERATORS, + operators: DATE_FILTER_OPERATORS, ui: { label: 'Paid start date', type: 'date', - defaultOperator: 'is-or-less', + defaultOperator: DEFAULT_DATE_OPERATOR, className: 'w-40' }, metadata: { @@ -354,14 +274,14 @@ export const memberFields = defineFields({ include: 'subscriptions' } }, - codec: memberDateCodec + codec: dateCodec() }, 'subscriptions.current_period_end': { - operators: DATE_OPERATORS, + operators: DATE_FILTER_OPERATORS, ui: { label: 'Next billing date', type: 'date', - defaultOperator: 'is-or-less', + defaultOperator: DEFAULT_DATE_OPERATOR, className: 'w-40' }, metadata: { @@ -371,7 +291,7 @@ export const memberFields = defineFields({ include: 'subscriptions' } }, - codec: memberDateCodec + codec: dateCodec() }, conversion: { operators: SCALAR_OPERATORS, diff --git a/apps/posts/src/views/members/member-filter-query.ts b/apps/posts/src/views/members/member-filter-query.ts index 1e3fe4b27e6..02db932eb5c 100644 --- a/apps/posts/src/views/members/member-filter-query.ts +++ b/apps/posts/src/views/members/member-filter-query.ts @@ -1,15 +1,10 @@ -import {dispatchSimpleNodes, parseFilterToAst, serializePredicates, stampPredicates} from '../filters/filter-query-core'; +import {dispatchSimpleNodes, getFieldKeysByType, hasFieldKey, parseFilterToAst, serializePredicates, stampPredicates} from '../filters/filter-query-core'; import {memberFields} from './member-fields'; import type {AstNode} from '../filters/filter-ast'; import type {FilterPredicate, ParsedPredicate} from '../filters/filter-types'; type CompoundMatcher = (node: AstNode) => ParsedPredicate | null; -const TIMEZONE_SENSITIVE_MEMBER_FIELDS = new Set([ - 'last_seen_at', - 'created_at', - 'subscriptions.start_date', - 'subscriptions.current_period_end' -]); +const TIMEZONE_SENSITIVE_MEMBER_FIELDS = getFieldKeysByType(memberFields, 'date'); function getCompoundChildren(node: AstNode): {operator: '$and' | '$or'; children: AstNode[]} | null { if (Array.isArray(node.$and)) { @@ -179,28 +174,6 @@ const MEMBER_COMPOUND_MATCHERS: CompoundMatcher[] = [ matchFeedbackGroupedNode ]; -function hasTimezoneSensitiveMemberField(node: AstNode): boolean { - if (Object.keys(node).some(key => TIMEZONE_SENSITIVE_MEMBER_FIELDS.has(key))) { - return true; - } - - const compound = getCompoundChildren(node); - - if (compound) { - return compound.children.some(child => hasTimezoneSensitiveMemberField(child as AstNode)); - } - - return Object.values(node).some((value) => { - if (Array.isArray(value)) { - return value.some((child) => { - return child !== null && typeof child === 'object' && hasTimezoneSensitiveMemberField(child as AstNode); - }); - } - - return value !== null && typeof value === 'object' && hasTimezoneSensitiveMemberField(value as AstNode); - }); -} - function parseMemberNode(node: AstNode, timezone: string): ParsedPredicate[] { for (const matcher of MEMBER_COMPOUND_MATCHERS) { const parsed = matcher(node); @@ -236,7 +209,7 @@ export function hasTimezoneSensitiveMemberFilter(filter: string | undefined): bo return false; } - return hasTimezoneSensitiveMemberField(ast); + return hasFieldKey(ast, TIMEZONE_SENSITIVE_MEMBER_FIELDS); } export function serializeMemberFilters(predicates: FilterPredicate[], timezone: string): string | undefined { diff --git a/apps/posts/src/views/members/use-member-filter-fields.ts b/apps/posts/src/views/members/use-member-filter-fields.ts index 764d6dfbe69..9bcee2a7ed5 100644 --- a/apps/posts/src/views/members/use-member-filter-fields.ts +++ b/apps/posts/src/views/members/use-member-filter-fields.ts @@ -1,8 +1,10 @@ import React, {useMemo} from 'react'; -import moment from 'moment-timezone'; +import {DATE_OPERATOR_LABELS} from '../filters/filter-date'; import {FilterFieldConfig, FilterFieldGroup, FilterOption, ValueSource} from '@tryghost/shade/patterns'; import {LabelFilterRenderer} from '@src/components/label-picker'; import {LucideIcon} from '@tryghost/shade/utils'; +import {createOperatorOptions} from '../filters/filter-operator-options'; +import {getTodayInTimezone} from '../filters/filter-normalization'; import {memberFields} from './member-fields'; import type {Offer} from '@tryghost/admin-x-framework/api/offers'; @@ -27,31 +29,11 @@ interface UseMemberFilterFieldsOptions { type OfferOption = FilterOption; type SearchableFieldOverrides = Pick; -interface OperatorOption { - value: string; - label: string; -} - -function createOperatorOptions( - operators: readonly string[], - options: {labels?: Record} = {} -): OperatorOption[] { - const labels = options.labels || {}; - - return operators.map(operator => ({ - value: operator, - label: labels[operator] ?? operator.replaceAll('-', ' ') - })); -} - const MEMBER_OPERATOR_LABELS: Record = { 'is-any': 'is any of', 'is-not-any': 'is none of', 'does-not-contain': 'does not contain', - 'is-less': 'before', - 'is-or-less': 'on or before', - 'is-greater': 'after', - 'is-or-greater': 'on or after', + ...DATE_OPERATOR_LABELS, 1: 'More like this', 0: 'Less like this' }; @@ -311,7 +293,7 @@ export function useMemberFilterFields({ const hiddenHydratedNewsletters = visibleHydratedNewsletters.filter(newsletter => !activeNewsletterSlugs.has(newsletter.slug)); const offerOptions = buildOfferOptions(offers); const offerLabels = createOfferLabelMap(offers); - const today = moment.tz(siteTimezone).format('YYYY-MM-DD'); + const today = getTodayInTimezone(siteTimezone); const basicFields: FilterFieldConfig[] = [ createFieldConfig('name'), diff --git a/apps/posts/test/unit/hooks/create-combined-value-source.test.tsx b/apps/posts/test/unit/hooks/create-combined-value-source.test.tsx new file mode 100644 index 00000000000..79813f94dd5 --- /dev/null +++ b/apps/posts/test/unit/hooks/create-combined-value-source.test.tsx @@ -0,0 +1,111 @@ +import {createCombinedValueSource} from '@src/hooks/filter-sources/create-combined-value-source'; +import {createRemoteValueSource} from '@src/hooks/filter-sources/create-remote-value-source'; +import {describe, expect, it} from 'vitest'; +import {renderHook} from '@testing-library/react'; + +type TestItem = { + id: string; + label: string; +}; + +let firstBrowseData: TestItem[] | undefined; +let firstHydrateData: TestItem[] | undefined; +let secondBrowseData: TestItem[] | undefined; +let secondHydrateData: TestItem[] | undefined; + +const useFirstSource = createRemoteValueSource({ + id: 'first.remote', + useBrowse: () => ({ + data: firstBrowseData, + isLoading: false, + isRefreshing: false, + isLoadingMore: false, + hasMore: false, + loadMore: () => {} + }), + useHydrate: () => ({ + data: firstHydrateData, + isLoading: false + }), + toOption: item => ({ + value: item.id, + label: item.label + }) +}); + +const useSecondSource = createRemoteValueSource({ + id: 'second.remote', + useBrowse: () => ({ + data: secondBrowseData, + isLoading: false, + isRefreshing: false, + isLoadingMore: false, + hasMore: false, + loadMore: () => {} + }), + useHydrate: () => ({ + data: secondHydrateData, + isLoading: false + }), + toOption: item => ({ + value: item.id, + label: item.label + }) +}); + +const useCombinedSource = createCombinedValueSource( + useFirstSource, + useSecondSource, + value => ({ + value, + label: `ID: ${value}` + }) +); + +describe('createCombinedValueSource', () => { + it('prefers a hydrated option from one source over a fallback from another source', () => { + firstBrowseData = []; + firstHydrateData = []; + secondBrowseData = []; + secondHydrateData = [{id: 'page-id', label: 'About page'}]; + + const {result} = renderHook(() => { + const source = useCombinedSource(); + + return source.useOptions({ + query: '', + selectedValues: ['page-id'] + }); + }); + + expect(result.current.options).toEqual([ + { + value: 'page-id', + label: 'About page' + } + ]); + }); + + it('adds a fallback option when neither source can hydrate a selected value', () => { + firstBrowseData = []; + firstHydrateData = []; + secondBrowseData = []; + secondHydrateData = []; + + const {result} = renderHook(() => { + const source = useCombinedSource(); + + return source.useOptions({ + query: '', + selectedValues: ['missing-id'] + }); + }); + + expect(result.current.options).toEqual([ + { + value: 'missing-id', + label: 'ID: missing-id' + } + ]); + }); +}); diff --git a/apps/posts/test/unit/hooks/create-remote-value-source.test.tsx b/apps/posts/test/unit/hooks/create-remote-value-source.test.tsx new file mode 100644 index 00000000000..dd428a0e6cb --- /dev/null +++ b/apps/posts/test/unit/hooks/create-remote-value-source.test.tsx @@ -0,0 +1,58 @@ +import {createRemoteValueSource} from '@src/hooks/filter-sources/create-remote-value-source'; +import {describe, expect, it} from 'vitest'; +import {renderHook} from '@testing-library/react'; + +type TestItem = { + id: string; + label: string; +}; + +let browseData: TestItem[] | undefined; +let hydrateData: TestItem[] | undefined; + +const useTestSource = createRemoteValueSource({ + id: 'test.remote', + useBrowse: () => ({ + data: browseData, + isLoading: false, + isRefreshing: false, + isLoadingMore: false, + hasMore: false, + loadMore: () => {} + }), + useHydrate: () => ({ + data: hydrateData, + isLoading: false + }), + toOption: item => ({ + value: item.id, + label: item.label + }), + getMissingSelectedOption: value => ({ + value, + label: `ID: ${value}` + }) +}); + +describe('createRemoteValueSource', () => { + it('keeps a fallback option for selected values that cannot be hydrated', () => { + browseData = []; + hydrateData = []; + + const {result} = renderHook(() => { + const source = useTestSource(); + + return source.useOptions({ + query: '', + selectedValues: ['missing-id'] + }); + }); + + expect(result.current.options).toEqual([ + { + value: 'missing-id', + label: 'ID: missing-id' + } + ]); + }); +}); diff --git a/apps/posts/test/unit/utils/filter-normalization.test.ts b/apps/posts/test/unit/utils/filter-normalization.test.ts new file mode 100644 index 00000000000..94596e06a3c --- /dev/null +++ b/apps/posts/test/unit/utils/filter-normalization.test.ts @@ -0,0 +1,39 @@ +import {describe, expect, it} from 'vitest'; +import {escapeNqlString, formatDateInTimezone, getDayBoundsInUtc} from '@src/views/filters/filter-normalization'; + +describe('filter-normalization', () => { + it('escapes single quotes for NQL strings', () => { + expect(escapeNqlString('can\'t stop')).toBe('\'can\\\'t stop\''); + }); + + it('escapes backslashes before single quotes for NQL strings', () => { + expect(escapeNqlString('test\\\'value')).toBe('\'test\\\\\\\'value\''); + }); + + it('computes UTC day bounds from a site timezone date', () => { + expect(getDayBoundsInUtc('2024-02-01', 'America/New_York')).toEqual({ + start: '2024-02-01T05:00:00.000Z', + end: '2024-02-02T04:59:59.999Z' + }); + }); + + it('computes shorter UTC day bounds across spring-forward DST transitions', () => { + expect(getDayBoundsInUtc('2024-03-10', 'America/New_York')).toEqual({ + start: '2024-03-10T05:00:00.000Z', + end: '2024-03-11T03:59:59.999Z' + }); + }); + + it('formats ISO instants in a site timezone', () => { + expect(formatDateInTimezone('2024-02-01T22:59:59.999Z', 'Europe/Stockholm')).toBe('2024-02-01'); + expect(formatDateInTimezone('2024-02-01T23:00:00.000Z', 'Europe/Stockholm')).toBe('2024-02-02'); + }); + + it('formats legacy UTC date-times in a site timezone', () => { + expect(formatDateInTimezone('2022-02-01 23:59:59', 'Europe/Stockholm')).toBe('2022-02-02'); + }); + + it('ignores invalid date values', () => { + expect(formatDateInTimezone('not-a-date', 'UTC')).toBeNull(); + }); +}); diff --git a/apps/posts/test/unit/views/comments/comment-fields.test.ts b/apps/posts/test/unit/views/comments/comment-fields.test.ts new file mode 100644 index 00000000000..08183b2c49a --- /dev/null +++ b/apps/posts/test/unit/views/comments/comment-fields.test.ts @@ -0,0 +1,143 @@ +import nql from '@tryghost/nql-lang'; +import {commentFields} from '@src/views/comments/comment-fields'; +import {describe, expect, it} from 'vitest'; +import type {CodecContext, FilterPredicate} from '@src/views/filters/filter-types'; + +const createdAtContext: CodecContext = { + key: 'created_at', + pattern: 'created_at', + params: {}, + timezone: 'UTC' +}; + +const reportedContext: CodecContext = { + key: 'reported', + pattern: 'reported', + params: {}, + timezone: 'UTC' +}; + +const bodyContext: CodecContext = { + key: 'body', + pattern: 'body', + params: {}, + timezone: 'UTC' +}; + +describe('commentFields', () => { + it('defines the expected comment field set', () => { + expect(Object.keys(commentFields)).toEqual([ + 'status', + 'created_at', + 'body', + 'post', + 'author', + 'reported' + ]); + }); + + it('keeps the expected operators for key comment fields', () => { + expect(commentFields.status.operators).toEqual(['is']); + expect(commentFields.created_at.operators).toEqual([ + 'is-less', + 'is-or-less', + 'is-greater', + 'is-or-greater' + ]); + expect(commentFields.body.operators).toEqual(['contains', 'does-not-contain']); + expect(commentFields.post.operators).toEqual(['is', 'is-not']); + expect(commentFields.author.operators).toEqual(['is', 'is-not']); + expect(commentFields.reported.operators).toEqual(['is']); + }); + + it('keeps parse aliases local to mapped comment fields', () => { + expect(commentFields.body.parseKeys).toEqual(['html']); + expect(commentFields.post.parseKeys).toEqual(['post_id']); + expect(commentFields.author.parseKeys).toEqual(['member_id']); + expect(commentFields.reported.parseKeys).toEqual(['count.reports']); + }); + + describe('commentDateCodec', () => { + it('serializes dates with member-compatible UTC day boundaries', () => { + const predicate: FilterPredicate = { + id: '1', + field: 'created_at', + operator: 'is-or-less', + values: ['2024-01-01'] + }; + + expect(commentFields.created_at.codec.serialize(predicate, createdAtContext)).toEqual([ + 'created_at:<=\'2024-01-01T23:59:59.999Z\'' + ]); + }); + + it('parses date comparators back to local dates', () => { + expect(commentFields.created_at.codec.parse( + nql.parse('created_at:<\'2024-01-03T00:00:00.000Z\'') as never, + createdAtContext + )).toEqual({ + field: 'created_at', + operator: 'is-less', + values: ['2024-01-03'] + }); + + expect(commentFields.created_at.codec.parse( + nql.parse('created_at:>\'2024-01-01T23:59:59.999Z\'') as never, + createdAtContext + )).toEqual({ + field: 'created_at', + operator: 'is-greater', + values: ['2024-01-01'] + }); + }); + }); + + describe('reportedCodec', () => { + it('serializes reported yes/no state', () => { + expect(commentFields.reported.codec.serialize({ + id: '1', + field: 'reported', + operator: 'is', + values: ['true'] + }, reportedContext)).toEqual(['count.reports:>0']); + + expect(commentFields.reported.codec.serialize({ + id: '2', + field: 'reported', + operator: 'is', + values: ['false'] + }, reportedContext)).toEqual(['count.reports:0']); + }); + + it('parses count-based report filters into boolean values', () => { + expect(commentFields.reported.codec.parse( + nql.parse('count.reports:>0') as never, + reportedContext + )).toEqual({ + field: 'reported', + operator: 'is', + values: ['true'] + }); + + expect(commentFields.reported.codec.parse( + nql.parse('count.reports:0') as never, + reportedContext + )).toEqual({ + field: 'reported', + operator: 'is', + values: ['false'] + }); + }); + }); + + describe('mapped shared codecs', () => { + it('uses the shared text codec with the html field override', () => { + expect(commentFields.body.codec.serialize({ + id: '1', + field: 'body', + operator: 'does-not-contain', + values: ['ghost'] + }, bodyContext)).toEqual(['html:-~\'ghost\'']); + }); + }); +}); diff --git a/apps/posts/test/unit/views/comments/comment-filter-query.test.ts b/apps/posts/test/unit/views/comments/comment-filter-query.test.ts new file mode 100644 index 00000000000..604fb0359d9 --- /dev/null +++ b/apps/posts/test/unit/views/comments/comment-filter-query.test.ts @@ -0,0 +1,175 @@ +import {describe, expect, it} from 'vitest'; +import {parseCommentFilter, serializeCommentFilters} from '@src/views/comments/comment-filter-query'; +import type {FilterPredicate} from '@src/views/filters/filter-types'; + +function stripIds(predicates: FilterPredicate[]) { + return predicates.map(predicate => ({ + field: predicate.field, + operator: predicate.operator, + values: predicate.values + })); +} + +describe('comment-filter-query', () => { + it('parses date compounds with member-compatible operators', () => { + const predicates = parseCommentFilter( + 'created_at:>=\'2024-01-01T00:00:00.000Z\'+created_at:<=\'2024-01-01T23:59:59.999Z\'', + 'UTC' + ); + + expect(stripIds(predicates)).toEqual([ + { + field: 'created_at', + operator: 'is-or-greater', + values: ['2024-01-01'] + }, + { + field: 'created_at', + operator: 'is-or-less', + values: ['2024-01-01'] + } + ]); + }); + + it('serializes date filters canonically', () => { + const predicates: FilterPredicate[] = [ + { + id: '1', + field: 'created_at', + operator: 'is-or-less', + values: ['2024-01-01'] + } + ]; + + expect(serializeCommentFilters(predicates, 'UTC')).toBe( + 'created_at:<=\'2024-01-01T23:59:59.999Z\'' + ); + }); + + it('round-trips comment filters in a non-UTC site timezone', () => { + const parsed = parseCommentFilter( + 'created_at:>=\'2024-01-01T05:00:00.000Z\'+created_at:<=\'2024-01-02T04:59:59.999Z\'+member_id:member_123+status:published', + 'America/New_York' + ); + + expect(stripIds(parsed)).toEqual([ + { + field: 'created_at', + operator: 'is-or-greater', + values: ['2024-01-01'] + }, + { + field: 'created_at', + operator: 'is-or-less', + values: ['2024-01-01'] + }, + { + field: 'author', + operator: 'is', + values: ['member_123'] + }, + { + field: 'status', + operator: 'is', + values: ['published'] + } + ]); + + expect(serializeCommentFilters(parsed, 'America/New_York')).toBe( + 'created_at:<=\'2024-01-02T04:59:59.999Z\'+created_at:>=\'2024-01-01T05:00:00.000Z\'+member_id:member_123+status:published' + ); + }); + + it('round-trips date boundaries across DST transitions', () => { + const parsed = parseCommentFilter( + 'created_at:>=\'2024-03-10T05:00:00.000Z\'+created_at:<=\'2024-03-11T03:59:59.999Z\'', + 'America/New_York' + ); + + expect(stripIds(parsed)).toEqual([ + { + field: 'created_at', + operator: 'is-or-greater', + values: ['2024-03-10'] + }, + { + field: 'created_at', + operator: 'is-or-less', + values: ['2024-03-10'] + } + ]); + + expect(serializeCommentFilters(parsed, 'America/New_York')).toBe( + 'created_at:<=\'2024-03-11T03:59:59.999Z\'+created_at:>=\'2024-03-10T05:00:00.000Z\'' + ); + }); + + it('parses and serializes reported filters', () => { + const parsed = parseCommentFilter('count.reports:>0', 'UTC'); + + expect(stripIds(parsed)).toEqual([ + { + field: 'reported', + operator: 'is', + values: ['true'] + } + ]); + + expect(serializeCommentFilters(parsed, 'UTC')).toBe('count.reports:>0'); + }); + + it('round-trips mapped comment fields through canonical NQL', () => { + const parsed = parseCommentFilter( + 'count.reports:0+html:~\'ghost\'+member_id:member_123+post_id:post_456+status:hidden', + 'UTC' + ); + + expect(stripIds(parsed)).toEqual([ + { + field: 'reported', + operator: 'is', + values: ['false'] + }, + { + field: 'body', + operator: 'contains', + values: ['ghost'] + }, + { + field: 'author', + operator: 'is', + values: ['member_123'] + }, + { + field: 'post', + operator: 'is', + values: ['post_456'] + }, + { + field: 'status', + operator: 'is', + values: ['hidden'] + } + ]); + + expect(serializeCommentFilters(parsed, 'UTC')).toBe( + 'count.reports:0+html:~\'ghost\'+member_id:member_123+post_id:post_456+status:hidden' + ); + }); + + it('drops unsupported fields such as id from canonical parsing', () => { + const parsed = parseCommentFilter('id:comment_123+status:published', 'UTC'); + + expect(stripIds(parsed)).toEqual([ + { + field: 'status', + operator: 'is', + values: ['published'] + } + ]); + }); + + it('ignores malformed NQL input', () => { + expect(parseCommentFilter('created_at:(', 'UTC')).toEqual([]); + }); +}); diff --git a/apps/posts/test/unit/views/comments/use-comment-filter-fields.test.tsx b/apps/posts/test/unit/views/comments/use-comment-filter-fields.test.tsx new file mode 100644 index 00000000000..759d9e9515c --- /dev/null +++ b/apps/posts/test/unit/views/comments/use-comment-filter-fields.test.tsx @@ -0,0 +1,43 @@ +import {afterEach, describe, expect, it, vi} from 'vitest'; +import {renderHook} from '@testing-library/react'; +import {useCommentFilterFields} from '@src/views/comments/use-comment-filter-fields'; +import type {ValueSource} from '@tryghost/shade/patterns'; + +const emptyValueSource: ValueSource = { + id: 'empty', + useOptions: () => ({ + options: [], + isInitialLoad: false, + isSearching: false, + isLoadingMore: false, + hasMore: false, + loadMore: vi.fn() + }) +}; + +describe('useCommentFilterFields', () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it('sets date filter defaults in the site timezone', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2024-03-10T06:00:00.000Z')); + + const {result} = renderHook(() => useCommentFilterFields({ + memberValueSource: emptyValueSource, + postValueSource: emptyValueSource, + siteTimezone: 'America/Los_Angeles' + })); + + expect(result.current.find(field => field.key === 'created_at')).toMatchObject({ + defaultValue: '2024-03-09', + operators: [ + {value: 'is-less', label: 'before'}, + {value: 'is-or-less', label: 'on or before'}, + {value: 'is-greater', label: 'after'}, + {value: 'is-or-greater', label: 'on or after'} + ] + }); + }); +}); diff --git a/apps/posts/test/unit/views/comments/use-filter-state.test.tsx b/apps/posts/test/unit/views/comments/use-filter-state.test.tsx new file mode 100644 index 00000000000..3dc9892894c --- /dev/null +++ b/apps/posts/test/unit/views/comments/use-filter-state.test.tsx @@ -0,0 +1,171 @@ +// @vitest-environment jsdom + +import {MemoryRouter, useSearchParams} from 'react-router'; +import {act, renderHook} from '@testing-library/react'; +import {describe, expect, it} from 'vitest'; +import {shouldDelayCommentDateFilterHydration, useFilterState} from '@src/views/comments/hooks/use-filter-state'; +import type {ReactNode} from 'react'; + +function createWrapper(initialEntry: string) { + return function Wrapper({children}: {children: ReactNode}) { + return {children}; + }; +} + +describe('use-filter-state', () => { + describe('shouldDelayCommentDateFilterHydration', () => { + it('waits for timezone resolution when date filters are present', () => { + expect(shouldDelayCommentDateFilterHydration('created_at:<=\'2024-02-01T22:59:59.999Z\'', false, true)).toBe(true); + }); + + it('does not wait for non-date filters', () => { + expect(shouldDelayCommentDateFilterHydration('status:published+count.reports:>0', false, true)).toBe(false); + }); + }); + + describe('useFilterState', () => { + it('reads canonical filter params', () => { + const {result} = renderHook(() => useFilterState('UTC'), { + wrapper: createWrapper('/?filter=status:published') + }); + + expect(result.current.filters).toEqual([ + { + id: 'status:1', + field: 'status', + operator: 'is', + values: ['published'] + } + ]); + expect(result.current.nql).toBe('status:published'); + }); + + it('migrates legacy per-field filter params to canonical filters', () => { + const {result} = renderHook(() => { + const state = useFilterState('UTC'); + const [searchParams] = useSearchParams(); + + return { + ...state, + query: searchParams.toString() + }; + }, {wrapper: createWrapper('/?status=is:hidden&body=not_contains:spam&author=is_not:member_123&created_at=before:2024-02-01')}); + + expect(result.current.filters).toEqual([ + { + id: 'status:1', + field: 'status', + operator: 'is', + values: ['hidden'] + }, + { + id: 'body:2', + field: 'body', + operator: 'does-not-contain', + values: ['spam'] + }, + { + id: 'author:3', + field: 'author', + operator: 'is-not', + values: ['member_123'] + }, + { + id: 'created_at:4', + field: 'created_at', + operator: 'is-less', + values: ['2024-02-01'] + } + ]); + expect(result.current.query).toBe('filter=created_at%3A%3C%272024-02-01T00%3A00%3A00.000Z%27%2Bhtml%3A-%7E%27spam%27%2Bmember_id%3A-member_123%2Bstatus%3Ahidden'); + }); + + it('ignores legacy id params because single-comment mode is handled outside filter state', () => { + const {result} = renderHook(() => useFilterState('UTC'), { + wrapper: createWrapper('/?id=is:comment_123') + }); + + expect(result.current.filters).toEqual([]); + expect(result.current.nql).toBeUndefined(); + }); + + it('writes canonical filter params while preserving unrelated query params', () => { + const {result} = renderHook(() => { + const state = useFilterState('UTC'); + const [searchParams] = useSearchParams(); + + return { + ...state, + query: searchParams.toString() + }; + }, {wrapper: createWrapper('/?thread=is:comment_123')}); + + act(() => { + result.current.setFilters([ + { + id: '1', + field: 'reported', + operator: 'is', + values: ['true'] + } + ], {replace: false}); + }); + + expect(result.current.query).toBe('thread=is%3Acomment_123&filter=count.reports%3A%3E0'); + }); + + it('keeps draft filters that are not serializable yet', () => { + const {result} = renderHook(() => { + const state = useFilterState('UTC'); + const [searchParams] = useSearchParams(); + + return { + ...state, + query: searchParams.toString() + }; + }, {wrapper: createWrapper('/?thread=is:comment_123')}); + + act(() => { + result.current.setFilters([ + { + id: '1', + field: 'body', + operator: 'contains', + values: [''] + } + ], {replace: false}); + }); + + expect(result.current.filters).toEqual([ + { + id: '1', + field: 'body', + operator: 'contains', + values: [''] + } + ]); + expect(result.current.nql).toBeUndefined(); + expect(result.current.query).toBe('thread=is%3Acomment_123'); + }); + + it('removes only the canonical filter param when clearing filters', () => { + const {result} = renderHook(() => { + const state = useFilterState('UTC'); + const [searchParams] = useSearchParams(); + + return { + ...state, + query: searchParams.toString() + }; + }, {wrapper: createWrapper('/?filter=status:published&id=is:comment_123&thread=is:comment_456')}); + + act(() => { + result.current.clearFilters({replace: false}); + }); + + expect(result.current.query).toBe('id=is%3Acomment_123&thread=is%3Acomment_456'); + expect(result.current.filters).toEqual([]); + expect(result.current.nql).toBeUndefined(); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a82b89fe6a..b6dc27e9e04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1000,6 +1000,9 @@ importers: sonner: specifier: 2.0.7 version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + temporal-polyfill: + specifier: 0.3.0 + version: 0.3.0 use-debounce: specifier: 10.1.1 version: 10.1.1(react@18.3.1) @@ -20694,6 +20697,12 @@ packages: resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==} engines: {node: '>=20'} + temporal-polyfill@0.3.0: + resolution: {integrity: sha512-qNsTkX9K8hi+FHDfHmf22e/OGuXmfBm9RqNismxBrnSmZVJKegQ+HYYXT+R7Ha8F/YSm2Y34vmzD4cxMu2u95g==} + + temporal-spec@0.3.0: + resolution: {integrity: sha512-n+noVpIqz4hYgFSMOSiINNOUOMFtV5cZQNCmmszA6GiVFVRt3G7AqVyhXjhCSmowvQn+NsGn+jMDMKJYHd3bSQ==} + terser-webpack-plugin@1.4.6: resolution: {integrity: sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==} engines: {node: '>= 6.9.0'} @@ -46288,6 +46297,12 @@ snapshots: ansi-escapes: 7.3.0 supports-hyperlinks: 4.4.0 + temporal-polyfill@0.3.0: + dependencies: + temporal-spec: 0.3.0 + + temporal-spec@0.3.0: {} + terser-webpack-plugin@1.4.6(webpack@4.47.0): dependencies: cacache: 12.0.4 From 9ee5b4332cf2916cdae81666b2dcb6b58879f1fc Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 6 May 2026 13:05:34 -0500 Subject: [PATCH 02/12] Removed unused rollup-plugin-node-builtins devDep (#27709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref `rollup-plugin-node-builtins` was declared as a devDep in `apps/admin-x-design-system`, `apps/shade`, and `apps/signup-form` but **never imported** — not in any source file, not in any of the three workspaces' `vite.config.*`, not in the shared `@tryghost/admin-x-framework` vite config. Each `vite.config` only loads `@vitejs/plugin-react` and `vite-plugin-svgr`; nothing references the rollup plugin. It's vestigial — leftover from whatever previously needed it (likely an early build setup for browserifying node builtins for browser bundles), since refactored away. Modern vite handles the node-builtin externalization the plugin used to provide. --- apps/admin-x-design-system/package.json | 1 - apps/shade/package.json | 1 - apps/signup-form/package.json | 3 +- pnpm-lock.yaml | 275 ------------------------ 4 files changed, 1 insertion(+), 279 deletions(-) diff --git a/apps/admin-x-design-system/package.json b/apps/admin-x-design-system/package.json index ad74ca2364b..bb94d110a35 100644 --- a/apps/admin-x-design-system/package.json +++ b/apps/admin-x-design-system/package.json @@ -54,7 +54,6 @@ "postcss-import": "16.1.1", "react": "18.3.1", "react-dom": "18.3.1", - "rollup-plugin-node-builtins": "2.1.2", "sinon": "18.0.1", "storybook": "10.3.5", "tailwindcss": "4.2.1", diff --git a/apps/shade/package.json b/apps/shade/package.json index bd5cc275f8b..7ebfda2d9ec 100644 --- a/apps/shade/package.json +++ b/apps/shade/package.json @@ -96,7 +96,6 @@ "jsdom": "28.1.0", "lodash-es": "4.18.1", "postcss": "8.5.6", - "rollup-plugin-node-builtins": "2.1.2", "sinon": "18.0.1", "storybook": "10.3.5", "tailwindcss": "4.2.1", diff --git a/apps/signup-form/package.json b/apps/signup-form/package.json index d795a41be24..c79ca510263 100644 --- a/apps/signup-form/package.json +++ b/apps/signup-form/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/signup-form", - "version": "0.3.19", + "version": "0.3.20", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", @@ -55,7 +55,6 @@ "postcss": "8.5.6", "postcss-import": "16.1.1", "prop-types": "15.8.1", - "rollup-plugin-node-builtins": "2.1.2", "storybook": "10.3.5", "stylelint": "15.11.0", "tailwindcss": "3.4.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6dc27e9e04..30c1278c853 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -473,9 +473,6 @@ importers: react-dom: specifier: 18.3.1 version: 18.3.1(react@18.3.1) - rollup-plugin-node-builtins: - specifier: 2.1.2 - version: 2.1.2 sinon: specifier: 18.0.1 version: 18.0.1 @@ -1275,9 +1272,6 @@ importers: postcss: specifier: 8.5.6 version: 8.5.6 - rollup-plugin-node-builtins: - specifier: 2.1.2 - version: 2.1.2 sinon: specifier: 18.0.1 version: 18.0.1 @@ -1375,9 +1369,6 @@ importers: prop-types: specifier: 15.8.1 version: 15.8.1 - rollup-plugin-node-builtins: - specifier: 2.1.2 - version: 2.1.2 storybook: specifier: 10.3.5 version: 10.3.5(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -9565,10 +9556,6 @@ packages: abortcontroller-polyfill@1.7.8: resolution: {integrity: sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ==} - abstract-leveldown@0.12.4: - resolution: {integrity: sha512-TOod9d5RDExo6STLMGa+04HGkl+TlMfbDnTyN93/ETJ9DpQ0DaYLqcMZlbXvdc4W3vVo1Qrl+WhSp8zvDsJ+jA==} - deprecated: Superseded by abstract-level (https://github.com/Level/community#faq) - accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -10466,9 +10453,6 @@ packages: bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} - bl@0.8.2: - resolution: {integrity: sha512-pfqikmByp+lifZCS0p6j6KreV6kNU6Apzpm2nKOk+94cZb/jvle55+JxWiByUQ0Wo/+XnDXEy5MxxKMb6r0VIw==} - bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -10770,9 +10754,6 @@ packages: browserify-des@1.0.2: resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} - browserify-fs@1.0.0: - resolution: {integrity: sha512-8LqHRPuAEKvyTX34R6tsw4bO2ro6j9DmlYBhiYWHRM26Zv2cBw1fJOU0NeUQ0RkXkPn/PFBjhA0dm4AgaBurTg==} - browserify-rsa@4.1.1: resolution: {integrity: sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==} engines: {node: '>= 0.10'} @@ -10821,9 +10802,6 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer-es6@4.9.3: - resolution: {integrity: sha512-Ibt+oXxhmeYJSsCkODPqNpPmyegefiD8rfutH1NYGhMZQhSp95Rz7haemgnJ6dxa6LT+JLLbtgOMORRluwKktw==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -11255,9 +11233,6 @@ packages: clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} - clone@0.1.19: - resolution: {integrity: sha512-IO78I0y6JcSpEPHzK4obKdsL7E7oLdRVDVOLwr2Hkbjsb+Eoz0dxW6tef0WizoKu0gLC4oZSZuEF4U2K6w1WQw==} - clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -12437,10 +12412,6 @@ packages: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} - deferred-leveldown@0.2.0: - resolution: {integrity: sha512-+WCbb4+ez/SZ77Sdy1iadagFiVzMB89IKOBhglgnUkVxOxRWmmFsz8UDSNWh4Rhq+3wr/vMFlYj+rdEwWUDdng==} - deprecated: Superseded by abstract-level (https://github.com/Level/community#faq) - define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -14147,9 +14118,6 @@ packages: resolution: {integrity: sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==} engines: {node: '>=0.10.0'} - foreach@2.0.6: - resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -14310,9 +14278,6 @@ packages: resolution: {integrity: sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==} engines: {node: '>=10'} - fwd-stream@1.0.4: - resolution: {integrity: sha512-q2qaK2B38W07wfPSQDKMiKOD5Nzv2XyuvQlrmh1q0pxyHNanKHq8lwQ6n9zHucAwA5EbzRJKEgds2orn88rYTg==} - gauge@2.7.4: resolution: {integrity: sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==} deprecated: This package is no longer supported. @@ -14939,9 +14904,6 @@ packages: peerDependencies: postcss: ^8.1.0 - idb-wrapper@1.7.2: - resolution: {integrity: sha512-zfNREywMuf0NzDo9mVsL0yegjsirJxHpKHvWcyRozIqQy89g0a3U+oBPOCN4cc0oCiOuYgZHimzaW/R46G1Mpg==} - ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -15022,9 +14984,6 @@ packages: indexes-of@1.0.1: resolution: {integrity: sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==} - indexof@0.0.1: - resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==} - infer-owner@1.0.4: resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} @@ -15331,9 +15290,6 @@ packages: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} - is-object@0.1.2: - resolution: {integrity: sha512-GkfZZlIZtpkFrqyAXPQSRBMsaHAw+CgoKe2HXAkjd/sfoI9+hS8PT4wg2rJxdQyUKr7N2vHJbg7/jQtE5l5vBQ==} - is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} @@ -15508,9 +15464,6 @@ packages: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} - is@0.2.7: - resolution: {integrity: sha512-ajQCouIvkcSnl2iRdK70Jug9mohIHVX9uKpoWnl115ov0R5mzBvRrXxrnHbsA+8AdwCwc/sfw7HXmd4I5EJBdQ==} - isarray@0.0.1: resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} @@ -15524,9 +15477,6 @@ packages: resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} engines: {node: '>= 8.0.0'} - isbuffer@0.0.0: - resolution: {integrity: sha512-xU+NoHp+YtKQkaM2HsQchYn0sltxMxew0HavMfHbjnucBoTSGbw745tL+Z7QBANleWM1eEQMenEpi174mIeS4g==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -16162,35 +16112,6 @@ packages: engines: {node: '>=18'} hasBin: true - level-blobs@0.1.7: - resolution: {integrity: sha512-n0iYYCGozLd36m/Pzm206+brIgXP8mxPZazZ6ZvgKr+8YwOZ8/PPpYC5zMUu2qFygRN8RO6WC/HH3XWMW7RMVg==} - - level-filesystem@1.2.0: - resolution: {integrity: sha512-PhXDuCNYpngpxp3jwMT9AYBMgOvB6zxj3DeuIywNKmZqFj2djj9XfT2XDVslfqmo0Ip79cAd3SBy3FsfOZPJ1g==} - - level-fix-range@1.0.2: - resolution: {integrity: sha512-9llaVn6uqBiSlBP+wKiIEoBa01FwEISFgHSZiyec2S0KpyLUkGR4afW/FCZ/X8y+QJvzS0u4PGOlZDdh1/1avQ==} - - level-fix-range@2.0.0: - resolution: {integrity: sha512-WrLfGWgwWbYPrHsYzJau+5+te89dUbENBg3/lsxOs4p2tYOhCHjbgXxBAj4DFqp3k/XBwitcRXoCh8RoCogASA==} - - level-hooks@4.5.0: - resolution: {integrity: sha512-fxLNny/vL/G4PnkLhWsbHnEaRi+A/k8r5EH/M77npZwYL62RHi2fV0S824z3QdpAk6VTgisJwIRywzBHLK4ZVA==} - - level-js@2.2.4: - resolution: {integrity: sha512-lZtjt4ZwHE00UMC1vAb271p9qzg8vKlnDeXfIesH3zL0KxhHRDjClQLGLWhyR0nK4XARnd4wc/9eD1ffd4PshQ==} - deprecated: Superseded by browser-level (https://github.com/Level/community#faq) - - level-peek@1.0.6: - resolution: {integrity: sha512-TKEzH5TxROTjQxWMczt9sizVgnmJ4F3hotBI48xCTYvOKd/4gA/uY0XjKkhJFo6BMic8Tqjf6jFMLWeg3MAbqQ==} - - level-sublevel@5.2.3: - resolution: {integrity: sha512-tO8jrFp+QZYrxx/Gnmjawuh1UBiifpvKNAcm4KCogesWr1Nm2+ckARitf+Oo7xg4OHqMW76eAqQ204BoIlscjA==} - - levelup@0.18.6: - resolution: {integrity: sha512-uB0auyRqIVXx+hrpIUtol4VAPhLRcnxcOsd2i2m6rbFIDarO5dnrupLOStYYpEcu8ZT087Z9HEuYw1wjr6RL6Q==} - deprecated: Superseded by abstract-level (https://github.com/Level/community#faq) - leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -16651,9 +16572,6 @@ packages: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} - ltgt@2.2.1: - resolution: {integrity: sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==} - lucide-react@0.577.0: resolution: {integrity: sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==} peerDependencies: @@ -17592,13 +17510,6 @@ packages: resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} engines: {node: '>= 0.4'} - object-keys@0.2.0: - resolution: {integrity: sha512-XODjdR2pBh/1qrjPcbSeSgEtKbYo7LqYNq64/TPuCf7j9SfDD3i21yatKoIy39yIWNvVM59iutfQQpCv1RfFzA==} - deprecated: Please update to the latest object-keys - - object-keys@0.4.0: - resolution: {integrity: sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==} - object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -17638,9 +17549,6 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - octal@1.0.0: - resolution: {integrity: sha512-nnda7W8d+A3vEIY+UrDQzzboPf1vhs4JYVhff5CDkq9QNoZY7Xrxeo/htox37j9dZf7yNHevZzqtejWgy1vCqQ==} - on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -18877,9 +18785,6 @@ packages: resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} engines: {node: ^20.17.0 || >=22.9.0} - process-es6@0.11.6: - resolution: {integrity: sha512-GYBRQtL4v3wgigq10Pv58jmTbFXlIiTbSfgnNqZLY0ldUPqy1rRxDI5fCjoCpnM6TqmHQI8ydzTBXW86OYc0gA==} - process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -19007,9 +18912,6 @@ packages: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} - prr@0.0.0: - resolution: {integrity: sha512-LmUECmrW7RVj6mDWKjTXfKug7TFGdiz9P18HMcO4RHL+RW7MCOGNvpj5j47Rnp6ne6r4fZ2VzyUWEpKbg+tsjQ==} - prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} @@ -19327,9 +19229,6 @@ packages: readable-stream@1.0.34: resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} - readable-stream@1.1.14: - resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -19685,9 +19584,6 @@ packages: resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} engines: {node: '>= 0.8'} - rollup-plugin-node-builtins@2.1.2: - resolution: {integrity: sha512-bxdnJw8jIivr2yEyt8IZSGqZkygIJOGAWypXvHXnwKAbUcN4Q/dGTx7K0oAJryC/m6aq6tKutltSeXtuogU6sw==} - rollup-pluginutils@2.8.2: resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} @@ -19853,10 +19749,6 @@ packages: selderee@0.6.0: resolution: {integrity: sha512-ibqWGV5aChDvfVdqNYuaJP/HnVBhlRGSRrlbttmlMpHcLuTqqbMH36QkSs9GEgj5M88JDYLI8eyP94JaQ8xRlg==} - semver@2.3.2: - resolution: {integrity: sha512-abLdIKCosKfpnmhS52NCTjO4RiLspDfsn37prjzGrp9im5DPJOgh82Os92vtwGh6XdQryKI/7SREZnV+aqiXrA==} - hasBin: true - semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -20318,9 +20210,6 @@ packages: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} - string-range@1.2.2: - resolution: {integrity: sha512-tYft6IFi8SjplJpxCUxyqisD3b+R2CSkomrtJYCkvuf1KuCAWgz7YXt4O0jip7efpfCemwHEzTEAO8EuOYgh3w==} - string-template@0.2.1: resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} @@ -21153,9 +21042,6 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typedarray-to-buffer@1.0.4: - resolution: {integrity: sha512-vjMKrfSoUDN8/Vnqitw2FmstOfuJ73G6CrSEKnf11A6RmasVxHqfeBcnTb6RsL4pTMuV5Zsv9IiHRphMZyckUw==} - typedarray-to-buffer@3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} @@ -22079,22 +21965,6 @@ packages: xregexp@2.0.0: resolution: {integrity: sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==} - xtend@2.0.6: - resolution: {integrity: sha512-fOZg4ECOlrMl+A6Msr7EIFcON1L26mb4NY5rurSkOex/TWhazOrg6eXD/B0XkuiYcYhQDWLXzQxLMVJ7LXwokg==} - engines: {node: '>=0.4'} - - xtend@2.1.2: - resolution: {integrity: sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==} - engines: {node: '>=0.4'} - - xtend@2.2.0: - resolution: {integrity: sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==} - engines: {node: '>=0.4'} - - xtend@3.0.0: - resolution: {integrity: sha512-sp/sT9OALMjRW1fKDlPeuSZlDQpkqReA0pyJukniWbTGoEKefHxhGJynE3PNhUMlcM8qWIjPwecwCw4LArS5Eg==} - engines: {node: '>=0.4'} - xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -31437,10 +31307,6 @@ snapshots: abortcontroller-polyfill@1.7.8: {} - abstract-leveldown@0.12.4: - dependencies: - xtend: 3.0.0 - accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -32647,10 +32513,6 @@ snapshots: bintrees@1.0.2: {} - bl@0.8.2: - dependencies: - readable-stream: 1.0.34 - bl@4.1.0: dependencies: buffer: 5.7.1 @@ -33367,12 +33229,6 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 - browserify-fs@1.0.0: - dependencies: - level-filesystem: 1.2.0 - level-js: 2.2.4 - levelup: 0.18.6 - browserify-rsa@4.1.1: dependencies: bn.js: 5.2.3 @@ -33441,8 +33297,6 @@ snapshots: buffer-equal-constant-time@1.0.1: {} - buffer-es6@4.9.3: {} - buffer-from@1.1.2: {} buffer-xor@1.0.3: {} @@ -33989,8 +33843,6 @@ snapshots: dependencies: mimic-response: 1.0.1 - clone@0.1.19: {} - clone@1.0.4: {} clone@2.1.2: {} @@ -34956,10 +34808,6 @@ snapshots: defer-to-connect@2.0.1: {} - deferred-leveldown@0.2.0: - dependencies: - abstract-leveldown: 0.12.4 - define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -38049,8 +37897,6 @@ snapshots: dependencies: for-in: 1.0.2 - foreach@2.0.6: {} - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -38271,10 +38117,6 @@ snapshots: fuse.js@6.6.2: {} - fwd-stream@1.0.4: - dependencies: - readable-stream: 1.0.34 - gauge@2.7.4: dependencies: aproba: 1.2.0 @@ -39081,8 +38923,6 @@ snapshots: dependencies: postcss: 8.5.6 - idb-wrapper@1.7.2: {} - ieee754@1.2.1: {} iferr@0.1.5: {} @@ -39147,8 +38987,6 @@ snapshots: indexes-of@1.0.1: {} - indexof@0.0.1: {} - infer-owner@1.0.4: {} inflected@2.1.0: {} @@ -39483,8 +39321,6 @@ snapshots: is-obj@2.0.0: {} - is-object@0.1.2: {} - is-path-inside@3.0.3: {} is-path-inside@4.0.0: {} @@ -39624,8 +39460,6 @@ snapshots: dependencies: is-inside-container: 1.0.0 - is@0.2.7: {} - isarray@0.0.1: {} isarray@1.0.0: {} @@ -39634,8 +39468,6 @@ snapshots: isbinaryfile@4.0.10: {} - isbuffer@0.0.0: {} - isexe@2.0.0: {} isexe@4.0.0: {} @@ -40767,64 +40599,6 @@ snapshots: source-map: 0.6.1 optional: true - level-blobs@0.1.7: - dependencies: - level-peek: 1.0.6 - once: 1.4.0 - readable-stream: 1.1.14 - - level-filesystem@1.2.0: - dependencies: - concat-stream: 1.6.2 - errno: 0.1.8 - fwd-stream: 1.0.4 - level-blobs: 0.1.7 - level-peek: 1.0.6 - level-sublevel: 5.2.3 - octal: 1.0.0 - once: 1.4.0 - xtend: 2.2.0 - - level-fix-range@1.0.2: {} - - level-fix-range@2.0.0: - dependencies: - clone: 0.1.19 - - level-hooks@4.5.0: - dependencies: - string-range: 1.2.2 - - level-js@2.2.4: - dependencies: - abstract-leveldown: 0.12.4 - idb-wrapper: 1.7.2 - isbuffer: 0.0.0 - ltgt: 2.2.1 - typedarray-to-buffer: 1.0.4 - xtend: 2.1.2 - - level-peek@1.0.6: - dependencies: - level-fix-range: 1.0.2 - - level-sublevel@5.2.3: - dependencies: - level-fix-range: 2.0.0 - level-hooks: 4.5.0 - string-range: 1.2.2 - xtend: 2.0.6 - - levelup@0.18.6: - dependencies: - bl: 0.8.2 - deferred-leveldown: 0.2.0 - errno: 0.1.8 - prr: 0.0.0 - readable-stream: 1.0.34 - semver: 2.3.2 - xtend: 3.0.0 - leven@3.1.0: {} levn@0.4.1: @@ -41274,8 +41048,6 @@ snapshots: lru.min@1.1.4: {} - ltgt@2.2.1: {} - lucide-react@0.577.0(react@18.3.1): dependencies: react: 18.3.1 @@ -42556,14 +42328,6 @@ snapshots: call-bind: 1.0.8 define-properties: 1.2.1 - object-keys@0.2.0: - dependencies: - foreach: 2.0.6 - indexof: 0.0.1 - is: 0.2.7 - - object-keys@0.4.0: {} - object-keys@1.1.1: {} object.assign@4.1.7: @@ -42624,8 +42388,6 @@ snapshots: obug@2.1.1: {} - octal@1.0.0: {} - on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -43892,8 +43654,6 @@ snapshots: proc-log@6.1.0: {} - process-es6@0.11.6: {} - process-nextick-args@2.0.1: {} process-relative-require@1.0.0: @@ -44074,8 +43834,6 @@ snapshots: proxy-from-env@2.1.0: {} - prr@0.0.0: {} - prr@1.0.1: {} psl@1.15.0: @@ -44448,13 +44206,6 @@ snapshots: isarray: 0.0.1 string_decoder: 0.10.31 - readable-stream@1.1.14: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 0.0.1 - string_decoder: 0.10.31 - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -44884,13 +44635,6 @@ snapshots: hash-base: 3.1.2 inherits: 2.0.4 - rollup-plugin-node-builtins@2.1.2: - dependencies: - browserify-fs: 1.0.0 - buffer-es6: 4.9.3 - crypto-browserify: 3.12.1 - process-es6: 0.11.6 - rollup-pluginutils@2.8.2: dependencies: estree-walker: 0.6.1 @@ -45127,8 +44871,6 @@ snapshots: dependencies: parseley: 0.7.0 - semver@2.3.2: {} - semver@5.7.2: {} semver@6.3.1: {} @@ -45747,8 +45489,6 @@ snapshots: char-regex: 1.0.2 strip-ansi: 6.0.1 - string-range@1.2.2: {} - string-template@0.2.1: {} string-width@1.0.2: @@ -46849,8 +46589,6 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typedarray-to-buffer@1.0.4: {} - typedarray-to-buffer@3.1.5: dependencies: is-typedarray: 1.0.0 @@ -48056,19 +47794,6 @@ snapshots: xregexp@2.0.0: {} - xtend@2.0.6: - dependencies: - is-object: 0.1.2 - object-keys: 0.2.0 - - xtend@2.1.2: - dependencies: - object-keys: 0.4.0 - - xtend@2.2.0: {} - - xtend@3.0.0: {} - xtend@4.0.2: {} y18n@4.0.3: {} From f0035b9baa9c95294a6136faefc8f01b9ee5c90d Mon Sep 17 00:00:00 2001 From: Chris Raible Date: Wed, 6 May 2026 12:16:50 -0700 Subject: [PATCH 03/12] Added `GET automations/:id` route with placeholder static payload (#27692) refs https://linear.app/ghost/issue/NY-1265/create-endpoint-for-reading-a-single-automation This adds the scaffolding for the GET `automations/:id` endpoint in the Admin API, with a hardcoded payload. The intent here is simply to unblock development of the frontend, while we iron out the rest of the schema. This will eventually be updated to return automations from the database, once the schema is in place. --- .secretlintrc.json | 2 +- .../core/server/api/endpoints/automations.js | 43 +++++++++++++++ .../server/web/api/endpoints/admin/routes.js | 1 + .../__snapshots__/automations.test.js.snap | 55 +++++++++++++++++++ .../test/e2e-api/admin/automations.test.js | 16 ++++++ .../unit/api/endpoints/automations.test.js | 41 ++++++++++++++ 6 files changed, 157 insertions(+), 1 deletion(-) diff --git a/.secretlintrc.json b/.secretlintrc.json index fcef33c49ab..abec5ba6fd3 100644 --- a/.secretlintrc.json +++ b/.secretlintrc.json @@ -16,7 +16,7 @@ { "name": "credential in URL query string", "patterns": [ - "/[?&](?:token|api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret|password)=(?(?!p\\.)[^&\\s\"'<>]{16,})/i" + "/[?&](?:token|api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret|password)=(?(?!p\\.|\\$\\{)[^&\\s\"'<>]{16,})/i" ] }, { diff --git a/ghost/core/core/server/api/endpoints/automations.js b/ghost/core/core/server/api/endpoints/automations.js index 71b56e7ca3f..30c6cff71b8 100644 --- a/ghost/core/core/server/api/endpoints/automations.js +++ b/ghost/core/core/server/api/endpoints/automations.js @@ -24,6 +24,49 @@ const controller = { } }, + read: { + headers: { + cacheInvalidate: false + }, + data: [ + 'id' + ], + permissions: true, + query(frame) { + // TODO: NY-1265 - replace this static payload with persisted automation data. + return { + id: frame.data.id, + slug: 'member-welcome-email-free', + name: 'Welcome email', + status: 'active', + created_at: '2026-05-05T00:00:00.000Z', + updated_at: '2026-05-05T00:00:00.000Z', + actions: [{ + id: '67f3f3f3f3f3f3f3f3f3f3f4', + type: 'delay', + data: { + delay_hours: 24 + } + }, { + id: '67f3f3f3f3f3f3f3f3f3f3f5', + type: 'send email', + data: { + email_subject: 'Welcome!', + email_lexical: '{"root":{"children":[]}}', + email_sender_name: null, + email_sender_email: null, + email_sender_reply_to: null, + email_design_setting_id: '680000000000000000000001' + } + }], + edges: [{ + source_action_id: '67f3f3f3f3f3f3f3f3f3f3f4', + target_action_id: '67f3f3f3f3f3f3f3f3f3f3f5' + }] + }; + } + }, + poll: { statusCode: 204, headers: { diff --git a/ghost/core/core/server/web/api/endpoints/admin/routes.js b/ghost/core/core/server/web/api/endpoints/admin/routes.js index e3a45181dab..2c796cc10f2 100644 --- a/ghost/core/core/server/web/api/endpoints/admin/routes.js +++ b/ghost/core/core/server/web/api/endpoints/admin/routes.js @@ -187,6 +187,7 @@ module.exports = function apiRoutes() { // ## Automations router.get('/automations', mw.authAdminApi, http(api.automations.browse)); + router.get('/automations/:id', mw.authAdminApi, http(api.automations.read)); router.put('/automations/poll', mw.authAdminApiWithUrl, http(api.automations.poll)); // ## Automated Emails diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/automations.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/automations.test.js.snap index c3e686e3ae2..8b0f0b7ffb0 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/automations.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/automations.test.js.snap @@ -72,3 +72,58 @@ Object { "x-powered-by": "Express", } `; + +exports[`Automations API read returns a placeholder automation for the requested id 1: [body] 1`] = ` +Object { + "automations": Array [ + Object { + "actions": Array [ + Object { + "data": Object { + "delay_hours": 24, + }, + "id": "67f3f3f3f3f3f3f3f3f3f3f4", + "type": "delay", + }, + Object { + "data": Object { + "email_design_setting_id": "680000000000000000000001", + "email_lexical": "{\\"root\\":{\\"children\\":[]}}", + "email_sender_email": null, + "email_sender_name": null, + "email_sender_reply_to": null, + "email_subject": "Welcome!", + }, + "id": "67f3f3f3f3f3f3f3f3f3f3f5", + "type": "send email", + }, + ], + "created_at": "2026-05-05T00:00:00.000Z", + "edges": Array [ + Object { + "source_action_id": "67f3f3f3f3f3f3f3f3f3f3f4", + "target_action_id": "67f3f3f3f3f3f3f3f3f3f3f5", + }, + ], + "id": "67f3f3f3f3f3f3f3f3f3f3f3", + "name": "Welcome email", + "slug": "member-welcome-email-free", + "status": "active", + "updated_at": "2026-05-05T00:00:00.000Z", + }, + ], +} +`; + +exports[`Automations API read returns a placeholder automation for the requested id 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "668", + "content-type": "application/json; charset=utf-8", + "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Version, Origin, Accept-Encoding", + "x-powered-by": "Express", +} +`; diff --git a/ghost/core/test/e2e-api/admin/automations.test.js b/ghost/core/test/e2e-api/admin/automations.test.js index 063069e0b68..240d3162de9 100644 --- a/ghost/core/test/e2e-api/admin/automations.test.js +++ b/ghost/core/test/e2e-api/admin/automations.test.js @@ -73,6 +73,22 @@ describe('Automations API', function () { }); }); + describe('read', function () { + it('returns a placeholder automation for the requested id', async function () { + const automationId = '67f3f3f3f3f3f3f3f3f3f3f3'; + + await agent + .get(`automations/${automationId}`) + .expectStatus(200) + .expect(cacheInvalidateHeaderNotSet()) + .matchBodySnapshot() + .matchHeaderSnapshot({ + 'content-version': anyContentVersion, + etag: anyEtag + }); + }); + }); + describe('poll', function () { /** @type {sinon.SinonStub} */ let dispatchStub; diff --git a/ghost/core/test/unit/api/endpoints/automations.test.js b/ghost/core/test/unit/api/endpoints/automations.test.js index 2d4fa98304f..f06b69a4aff 100644 --- a/ghost/core/test/unit/api/endpoints/automations.test.js +++ b/ghost/core/test/unit/api/endpoints/automations.test.js @@ -62,6 +62,47 @@ describe('Automations controller', function () { }); }); + describe('read', function () { + it('returns a placeholder automation for the requested id', function () { + const result = automationsController.read.query({ + data: { + id: '67f3f3f3f3f3f3f3f3f3f3f3' + } + }); + + assert.deepEqual(result, { + id: '67f3f3f3f3f3f3f3f3f3f3f3', + slug: 'member-welcome-email-free', + name: 'Welcome email', + status: 'active', + created_at: '2026-05-05T00:00:00.000Z', + updated_at: '2026-05-05T00:00:00.000Z', + actions: [{ + id: '67f3f3f3f3f3f3f3f3f3f3f4', + type: 'delay', + data: { + delay_hours: 24 + } + }, { + id: '67f3f3f3f3f3f3f3f3f3f3f5', + type: 'send email', + data: { + email_subject: 'Welcome!', + email_lexical: '{"root":{"children":[]}}', + email_sender_name: null, + email_sender_email: null, + email_sender_reply_to: null, + email_design_setting_id: '680000000000000000000001' + } + }], + edges: [{ + source_action_id: '67f3f3f3f3f3f3f3f3f3f3f4', + target_action_id: '67f3f3f3f3f3f3f3f3f3f3f5' + }] + }); + }); + }); + describe('poll', function () { it('dispatches a StartAutomationsPollEvent', function () { const result = automationsController.poll.query({}); From 697ac7bd0d028224ba5f29047a78da24ea4525b6 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 6 May 2026 14:18:38 -0500 Subject: [PATCH 04/12] Added knip for unused-dep detection (#27716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref Adds [knip](https://knip.dev/) at the workspace root with a minimal config so we can detect unused dependencies, files, and exports across the monorepo. This is wiring only — no source changes, no deletions. A follow-up PR will walk through the baseline report and remove the genuinely vestigial deps. --- knip.json | 10 + package.json | 3 + pnpm-lock.yaml | 581 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 582 insertions(+), 12 deletions(-) create mode 100644 knip.json diff --git a/knip.json b/knip.json new file mode 100644 index 00000000000..a0cad9dba42 --- /dev/null +++ b/knip.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "ignoreWorkspaces": [ + "ghost/admin" + ], + "ignoreDependencies": [ + "secretlint", + "@secretlint/.*" + ] +} diff --git a/package.json b/package.json index aefb7e1b909..31180740375 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ "docker:build": "docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} build", "docker:clean": "docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} --profile all down -v --remove-orphans --rmi local", "docker:down": "docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} down", + "knip": "knip", + "knip:fix": "knip --fix --allow-remove-files=false", "lint": "pnpm nx run-many -t lint", "test": "pnpm nx run-many -t test --exclude @tryghost/e2e --exclude ghost-admin", "test:unit": "pnpm nx run-many -t test:unit", @@ -141,6 +143,7 @@ "husky": "9.1.7", "inquirer": "8.2.7", "jsonc-parser": "3.3.1", + "knip": "6.12.0", "lint-staged": "16.4.0", "nx": "22.0.4", "rimraf": "6.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30c1278c853..b348d0fe194 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ importers: jsonc-parser: specifier: 3.3.1 version: 3.3.1 + knip: + specifier: 6.12.0 + version: 6.12.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) lint-staged: specifier: 16.4.0 version: 16.4.0 @@ -4108,15 +4111,24 @@ packages: '@glint/template': optional: true + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.9.1': resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.9.1': resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} '@emnapi/wasi-threads@1.2.0': resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -5313,6 +5325,12 @@ packages: '@napi-rs/wasm-runtime@0.2.4': resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} @@ -5649,6 +5667,244 @@ packages: '@otplib/preset-v11@12.0.1': resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==} + '@oxc-parser/binding-android-arm-eabi@0.128.0': + resolution: {integrity: sha512-aca6ZvzmCBUGOANQRiRQRZuRKYI3ENhcit6GisnknOOmcezfQc7xJ4dxlPU7MV7mOvrC7RNR1u3LAD7xyaiCxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.128.0': + resolution: {integrity: sha512-BbeDmuohoJ7Rz/it5wnkj69i/OsCPS3Z51nLEzwO/Y6YshtC4JU+15oNwhY8v4LRKRYclRc7ggOikwrsJ/eOEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.128.0': + resolution: {integrity: sha512-tRUHPt80417QmvNpoSslJT1VY8NUbWdrWR+L14Zn+RbOTcaqB8E6PYE/ZGN8jjWBzqporiA/H4MfO50ew/NCNA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.128.0': + resolution: {integrity: sha512-rWI2Hb1Nt3U/vKsjyNvZzDC8i/l144U20DKjhzaTmwIhIiSRGeroPWWiImwypmKLqrw8GuIixbWJkpGWLbkzrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.128.0': + resolution: {integrity: sha512-hhpdVMaNCLgQxjgNPeeFzSeJMmZPc5lKfv0NGSI3egZq9EdnEGqeC8JsYsQjK7PoQgbvZ17xlj0SO5ziH5Obkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.128.0': + resolution: {integrity: sha512-093zNw0zZ/e/obML+rhlSdmnzR0mVZluPcAkxunEc5E3F0yBVsFn24Y1ILfsEte11Ud041qn/gp2OJ1jxNqUng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.128.0': + resolution: {integrity: sha512-fq7DmKmfC+dvD97IXrgbph6Jzwe0EDu+PYMofmzZ6fv5X1k9vtaqLpDGMuICO9MmUnyKAQmVl+wIv2RNy4Dz8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.128.0': + resolution: {integrity: sha512-Xvm48jJah8TlIrURIjNOP/gNiGe6aKvCB+r06VliflFo8Kq7VOLE8PxtgShJzZIqubrgdMdYfvuPPozn7F6MbQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.128.0': + resolution: {integrity: sha512-M7iwBGmYJTx+pKOYFjI0buop4gJvlmcVzFGaXPt21DKpQkbQZG1f63Yg7LloIYT/t9yLxCw0Lhfx/RFlAlMSjA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.128.0': + resolution: {integrity: sha512-21LGNIZb1Pcfk5/EGsqabrxv4yqQOWis1407JJrClS7XpFCrbvr74YAB1V+m54cYbwvO6UWwQqS4WecxiyfCRg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.128.0': + resolution: {integrity: sha512-gyHjOTFpg9bTTYjxPmQirvufb89+VdZwVfcMtAUyPr6F5H8ZswvCQshK4qOW+Q+2Xyb33hduRgY/eFHJQjU/vQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.128.0': + resolution: {integrity: sha512-X6Q2oKUrP5GyDd2xniuEBLk6aFQCZ97W2+aVXGgJXdjx5t4/oFuA9ri0wLOUrBIX+qdSuK581snMBio4z910eA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.128.0': + resolution: {integrity: sha512-BdzTmqxfxoYkpgokoLaSnOX6T+R3/goL42klre2tnG+kHbG2TXS0VN+P5BPofH1axdKOHy5ei4ENZrjmCOt2lA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.128.0': + resolution: {integrity: sha512-OO1nW2Q7sSYYvJZpDHdvyFSdRaVcQqRijZSSmWVMqFxPYy8cEF45zJ9fcdIYuzIT3jYq6YRhEFm/VMWNWhE22Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.128.0': + resolution: {integrity: sha512-4NehAe404MRdoZVS9DW8C5XbJwbXIc/KfVlYdpi5vE4081zc9Y0YzKVqyOYj/Puye7/Do+ohaONBFWlEHYl9hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.128.0': + resolution: {integrity: sha512-kVbqgW9xLL8bh8oc7aYOJilRKXE5G33+tE0jan+duo/9OriaFRpijcCwT2waWs2oqYROYq0GlE7/p3ywoshVeg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.128.0': + resolution: {integrity: sha512-L38ojghJYHmgiz6fJd7jwLB/ESDBpB02NdFxh+smqVM6P2anCEvHn0jhaSrt5eVNR1Ak8+moOeftUlofeyvniA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.128.0': + resolution: {integrity: sha512-xgvO35GyHBtjlQ5AEpaYr7Rll1rvY7zqIhT6ty8E3ezBW2J1SFLjIDEvI/tcgDg6oaseDAqVcM+jU1HuCekgZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.128.0': + resolution: {integrity: sha512-OY+3eM2SN72prHKRB22mPz8o5A/7dJ+f5DFLBVvggyZhEaNDAH9IB+ElMjmOkOIwf5MDCUAowCK7pAncNxzpBA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.128.0': + resolution: {integrity: sha512-NE9ny+cPUCCObXa0IKLfj0tCdPd7pe/dz9ZpkxpUOymB3miNeMPybdlYYTBSGJUalMWeBM85/4JcCErCNTqOXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.128.0': + resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.19.1': + resolution: {integrity: sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + resolution: {integrity: sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.19.1': + resolution: {integrity: sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + resolution: {integrity: sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + resolution: {integrity: sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + resolution: {integrity: sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.19.1': + resolution: {integrity: sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + resolution: {integrity: sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + resolution: {integrity: sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + resolution: {integrity: sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==} + cpu: [x64] + os: [win32] + '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -8738,6 +8994,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} @@ -13891,6 +14150,9 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -14145,6 +14407,11 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + formidable@1.2.6: resolution: {integrity: sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==} deprecated: 'Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau' @@ -14366,6 +14633,9 @@ packages: get-tsconfig@4.13.7: resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + getopts@2.2.5: resolution: {integrity: sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA==} @@ -16082,6 +16352,11 @@ packages: tedious: optional: true + knip@6.12.0: + resolution: {integrity: sha512-nRg8+DOFcfBD6NjmNzu9+3D35QnEmMsnojJGOHQUqv+70r1aOx99wpSUXvEV7syQVOL5E6tNXXkoyG1Fuz8BWg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + known-css-properties@0.29.0: resolution: {integrity: sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==} @@ -17636,6 +17911,13 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxc-parser@0.128.0: + resolution: {integrity: sha512-XkOw3eiIxAgQ19WRew/Bq9wc5Ga/guaWIzDBzq80z1PyuDNGvWBpPby9k6YGwV8A8uMw+Nlq3xqlzuDYmUFYUw==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.19.1: + resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -19965,6 +20247,10 @@ packages: resolution: {integrity: sha512-0R6YJ5hLpDH4mZR7N5eZ12oCMLspvGOHL9A9SEm2e3b/CQmQidekW4SWSKEmor/3x6m3NCBBEqLzikcZC9VJNQ==} engines: {node: '>=4.0.0'} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} @@ -20332,6 +20618,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} @@ -20582,16 +20872,16 @@ packages: resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} engines: {node: '>=6.0.0'} - terminal-link@5.0.0: - resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==} - engines: {node: '>=20'} - temporal-polyfill@0.3.0: resolution: {integrity: sha512-qNsTkX9K8hi+FHDfHmf22e/OGuXmfBm9RqNismxBrnSmZVJKegQ+HYYXT+R7Ha8F/YSm2Y34vmzD4cxMu2u95g==} temporal-spec@0.3.0: resolution: {integrity: sha512-n+noVpIqz4hYgFSMOSiINNOUOMFtV5cZQNCmmszA6GiVFVRt3G7AqVyhXjhCSmowvQn+NsGn+jMDMKJYHd3bSQ==} + terminal-link@5.0.0: + resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==} + engines: {node: '>=20'} + terser-webpack-plugin@1.4.6: resolution: {integrity: sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==} engines: {node: '>= 6.9.0'} @@ -20717,6 +21007,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + tinypool@0.8.4: resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} engines: {node: '>=14.0.0'} @@ -21089,6 +21383,10 @@ packages: resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} engines: {node: '>= 0.8'} + unbash@3.0.0: + resolution: {integrity: sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA==} + engines: {node: '>=14'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -21679,6 +21977,10 @@ packages: resolution: {integrity: sha512-41TvKmDGVpm2iuH7o+DAOt06yyu/cSHpX3uzAwetzASvlNtVddgIjXIb2DfB/Wa20B1Jo86+1Dv1CraSU7hWdw==} engines: {node: 10.* || >= 12.*} + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -24498,11 +24800,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.9.1': dependencies: '@emnapi/wasi-threads': 1.2.0 tslib: 2.8.1 + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.1': dependencies: tslib: 2.8.1 @@ -24511,6 +24824,11 @@ snapshots: dependencies: tslib: 2.8.1 + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.28.6 @@ -25771,6 +26089,13 @@ snapshots: '@emnapi/runtime': 1.9.1 '@tybys/wasm-util': 0.9.0 + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': dependencies: eslint-scope: 5.1.1 @@ -26154,6 +26479,137 @@ snapshots: '@otplib/plugin-crypto': 12.0.1 '@otplib/plugin-thirty-two': 12.0.1 + '@oxc-parser/binding-android-arm-eabi@0.128.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.128.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.128.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.128.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.128.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.128.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.128.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.128.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.128.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.128.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.128.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.128.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.128.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.128.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.128.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.128.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.128.0': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.128.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.128.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.128.0': + optional: true + + '@oxc-project/types@0.128.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + optional: true + + '@oxc-resolver/binding-android-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + optional: true + '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 @@ -30101,6 +30557,11 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + '@tybys/wasm-util@0.9.0': dependencies: tslib: 2.8.1 @@ -30743,7 +31204,7 @@ snapshots: debug: 4.4.3(supports-color@5.5.0) minimatch: 9.0.9 semver: 7.7.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -30758,7 +31219,7 @@ snapshots: debug: 4.4.3(supports-color@5.5.0) minimatch: 10.2.4 semver: 7.7.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -37640,6 +38101,10 @@ snapshots: dependencies: bser: 2.1.1 + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -37933,6 +38398,10 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + formidable@1.2.6: {} formidable@2.1.5: @@ -38213,6 +38682,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + getopts@2.2.5: {} getopts@2.3.0: {} @@ -40556,6 +41029,26 @@ snapshots: transitivePeerDependencies: - supports-color + knip@6.12.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + formatly: 0.3.0 + get-tsconfig: 4.14.0 + jiti: 2.6.1 + minimist: 1.2.8 + oxc-parser: 0.128.0 + oxc-resolver: 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + picomatch: 4.0.4 + smol-toml: 1.6.1 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.16 + unbash: 3.0.0 + yaml: 2.8.3 + zod: 4.1.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + known-css-properties@0.29.0: {} language-subtag-registry@0.3.23: {} @@ -41968,7 +42461,7 @@ snapshots: proc-log: 6.1.0 semver: 7.7.4 tar: 7.5.13 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 which: 6.0.1 transitivePeerDependencies: - supports-color @@ -42503,6 +42996,57 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + oxc-parser@0.128.0: + dependencies: + '@oxc-project/types': 0.128.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.128.0 + '@oxc-parser/binding-android-arm64': 0.128.0 + '@oxc-parser/binding-darwin-arm64': 0.128.0 + '@oxc-parser/binding-darwin-x64': 0.128.0 + '@oxc-parser/binding-freebsd-x64': 0.128.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.128.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.128.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.128.0 + '@oxc-parser/binding-linux-arm64-musl': 0.128.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.128.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.128.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.128.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.128.0 + '@oxc-parser/binding-linux-x64-gnu': 0.128.0 + '@oxc-parser/binding-linux-x64-musl': 0.128.0 + '@oxc-parser/binding-openharmony-arm64': 0.128.0 + '@oxc-parser/binding-wasm32-wasi': 0.128.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.128.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.128.0 + '@oxc-parser/binding-win32-x64-msvc': 0.128.0 + + oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.19.1 + '@oxc-resolver/binding-android-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-x64': 11.19.1 + '@oxc-resolver/binding-freebsd-x64': 11.19.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.19.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.19.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-musl': 11.19.1 + '@oxc-resolver/binding-openharmony-arm64': 11.19.1 + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + p-cancelable@2.1.1: {} p-cancelable@3.0.0: {} @@ -45179,6 +45723,8 @@ snapshots: smartquotes@2.3.2: {} + smol-toml@1.6.1: {} + snake-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -45637,6 +46183,8 @@ snapshots: strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + strip-literal@2.1.1: dependencies: js-tokens: 9.0.1 @@ -46032,17 +46580,17 @@ snapshots: mkdirp: 0.5.6 rimraf: 2.6.3 - terminal-link@5.0.0: - dependencies: - ansi-escapes: 7.3.0 - supports-hyperlinks: 4.4.0 - temporal-polyfill@0.3.0: dependencies: temporal-spec: 0.3.0 temporal-spec@0.3.0: {} + terminal-link@5.0.0: + dependencies: + ansi-escapes: 7.3.0 + supports-hyperlinks: 4.4.0 + terser-webpack-plugin@1.4.6(webpack@4.47.0): dependencies: cacache: 12.0.4 @@ -46266,6 +46814,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinypool@0.8.4: {} tinypool@1.1.1: {} @@ -46637,6 +47190,8 @@ snapshots: dependencies: random-bytes: 1.0.0 + unbash@3.0.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -47424,6 +47979,8 @@ snapshots: matcher-collection: 2.0.1 minimatch: 3.1.5 + walk-up-path@4.0.0: {} + walker@1.0.8: dependencies: makeerror: 1.0.12 From d2b366d819829ca6b7ff351e48a49fa768c08de8 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 6 May 2026 14:25:12 -0500 Subject: [PATCH 05/12] Added fast-xml-parser override (#27719) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref `fast-xml-parser@5.5.8` was reachable via `ghost/core > @aws-sdk/client-s3 > @aws-sdk/core > @aws-sdk/xml-builder`. The advisory is a moderate XML comment / CDATA injection issue in `XMLBuilder` (the writer-side path), fixed upstream in `5.7.0`. The override forces the resolution up to `5.7.2` (latest 5.x patch). Same major (5.x), minor patch range bump; `@aws-sdk/xml-builder` uses `fast-xml-parser` the same way across 5.5 and 5.7 — no caller API changes. --- package.json | 1 + pnpm-lock.yaml | 31 ++----------------------------- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 31180740375..b0d9683307a 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "debug@<2.6.9": "^2.6.9", "diff@<3.5.1": "^3.5.1", "diff@>=6.0.0 <8.0.3": "^8.0.3", + "fast-xml-parser@<5.7.0": "^5.7.0", "follow-redirects@<1.16.0": "^1.16.0", "form-data@<2.5.4": "^2.5.4", "growl@<1.10.0": "^1.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b348d0fe194..3c024f44f1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,7 @@ overrides: debug@<2.6.9: ^2.6.9 diff@<3.5.1: ^3.5.1 diff@>=6.0.0 <8.0.3: ^8.0.3 + fast-xml-parser@<5.7.0: ^5.7.0 follow-redirects@<1.16.0: ^1.16.0 form-data@<2.5.4: ^2.5.4 growl@<1.10.0: ^1.10.0 @@ -14119,16 +14120,9 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-xml-builder@1.1.4: - resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} - fast-xml-builder@1.1.8: resolution: {integrity: sha512-sDVBc2gg8pSKvcbE8rBmOyjSGQf0AdsbqvHeIOv3D/uYNoV4eCReQXyDF8Pdv8+m1FHazACypSz2hR7O2S1LLw==} - fast-xml-parser@5.5.8: - resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} - hasBin: true - fast-xml-parser@5.7.2: resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==} hasBin: true @@ -18153,10 +18147,6 @@ packages: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - path-expression-matcher@1.2.0: - resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} - engines: {node: '>=14.0.0'} - path-expression-matcher@1.5.0: resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} engines: {node: '>=14.0.0'} @@ -20632,9 +20622,6 @@ packages: resolution: {integrity: sha512-hrA79fjmN2Eb6K3kxkDzU4ODeVGGjXQsuVaAPSUro6I9MM3X+BvIsVqdphm3BXWfimAGFvUqWtPtHy25mICY1w==} engines: {node: ^8.1 || >=10.*} - strnum@2.2.2: - resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==} - strnum@2.2.3: resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} @@ -23187,7 +23174,7 @@ snapshots: '@aws-sdk/xml-builder@3.972.16': dependencies: '@smithy/types': 4.13.1 - fast-xml-parser: 5.5.8 + fast-xml-parser: 5.7.2 tslib: 2.8.1 '@aws-sdk/xml-builder@3.972.22': @@ -38059,20 +38046,10 @@ snapshots: fast-uri@3.1.0: {} - fast-xml-builder@1.1.4: - dependencies: - path-expression-matcher: 1.2.0 - fast-xml-builder@1.1.8: dependencies: path-expression-matcher: 1.5.0 - fast-xml-parser@5.5.8: - dependencies: - fast-xml-builder: 1.1.4 - path-expression-matcher: 1.2.0 - strnum: 2.2.2 - fast-xml-parser@5.7.2: dependencies: '@nodable/entities': 2.1.0 @@ -43259,8 +43236,6 @@ snapshots: path-exists@5.0.0: {} - path-expression-matcher@1.2.0: {} - path-expression-matcher@1.5.0: {} path-is-absolute@1.0.1: {} @@ -46198,8 +46173,6 @@ snapshots: '@types/node': 25.6.0 qs: 6.15.0 - strnum@2.2.2: {} - strnum@2.2.3: {} strtok3@6.3.0: From c517a65549e449c98467e1e38c9c054a2e1d1dfd Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 6 May 2026 14:53:08 -0500 Subject: [PATCH 06/12] Moved Docker registry mirror setup before Buildx in CI (#27718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref - `setup-buildx-action` (default `docker-container` driver) bootstraps by pulling `moby/buildkit:buildx-stable-1` from Docker Hub. Intermittent Docker Hub auth timeouts (`auth.docker.io` deadline exceeded) have been taking down e2e shards before any tests run. - The `job_e2e_tests` job already configured `mirror.gcr.io` via the `setup-docker-registry-mirrors` action, but the step ran *after* Buildx — too late to help with the failing pull. The other two Buildx-using jobs (`job_build_artifacts`, `job_build_e2e_image`) had no mirror at all. - This PR moves the mirror step ahead of Buildx in all three jobs so the buildkit pull (and every subsequent Docker Hub pull in the job) is routed through the GCR pull-through cache, taking Docker Hub off the critical path. --- .github/workflows/ci.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1dcd899db2..66d16234089 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -968,6 +968,9 @@ jobs: retention-days: 7 if-no-files-found: error + - name: Setup Docker Registry Mirrors + uses: ./.github/actions/setup-docker-registry-mirrors + - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 @@ -1175,6 +1178,9 @@ jobs: - name: Extract public app artifacts run: tar -xzf e2e-public-apps.tar.gz + - name: Setup Docker Registry Mirrors + uses: ./.github/actions/setup-docker-registry-mirrors + - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 with: @@ -1275,6 +1281,9 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Docker Registry Mirrors + uses: ./.github/actions/setup-docker-registry-mirrors + - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 @@ -1298,9 +1307,6 @@ jobs: image-tags: ${{ needs.job_build_e2e_image.outputs.image-tags }} artifact-name: docker-image-e2e - - name: Setup Docker Registry Mirrors - uses: ./.github/actions/setup-docker-registry-mirrors - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: From 8f2ec8de4a685958c8a95652f9b72460c9f10131 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 6 May 2026 15:03:30 -0500 Subject: [PATCH 07/12] Removed unused dependencies surfaced by knip (#27720) no ref First cleanup pass after wiring up knip in #27716. Walked through each unused-dep candidate in the baseline report, grep-verified each one, and removed the genuinely vestigial entries. Net effect: 15 package.json files touched, **~390 lines off the lockfile**. --- apps/activitypub/package.json | 4 +- apps/admin-x-design-system/package.json | 1 - apps/admin-x-framework/package.json | 1 - apps/admin/package.json | 1 - apps/announcement-bar/package.json | 3 +- apps/comments-ui/package.json | 3 +- apps/portal/package.json | 3 +- apps/posts/package.json | 1 - apps/shade/package.json | 19 -- e2e/package.json | 1 - ghost/admin/package.json | 2 - ghost/core/package.json | 11 - ghost/parse-email-address/package.json | 2 - package.json | 4 - pnpm-lock.yaml | 375 ++---------------------- 15 files changed, 25 insertions(+), 406 deletions(-) diff --git a/apps/activitypub/package.json b/apps/activitypub/package.json index 8a828ecee4d..3ec8667791c 100644 --- a/apps/activitypub/package.json +++ b/apps/activitypub/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/activitypub", - "version": "3.1.14", + "version": "3.1.16", "license": "MIT", "repository": { "type": "git", @@ -39,13 +39,11 @@ "devDependencies": { "@playwright/test": "1.59.1", "@testing-library/react": "14.3.1", - "@types/dompurify": "3.2.0", "@types/jest": "29.5.14", "@types/react": "18.3.28", "@types/react-dom": "18.3.7", "jest": "29.7.0", "tailwindcss": "^4.2.2", - "ts-jest": "29.4.9", "vite": "5.4.21", "vitest": "1.6.1" }, diff --git a/apps/admin-x-design-system/package.json b/apps/admin-x-design-system/package.json index bb94d110a35..96757407195 100644 --- a/apps/admin-x-design-system/package.json +++ b/apps/admin-x-design-system/package.json @@ -28,7 +28,6 @@ "@codemirror/lang-html": "6.4.11", "@codemirror/state": "6.6.0", "@dnd-kit/utilities": "^3.2.2", - "@radix-ui/react-tooltip": "1.2.8", "@storybook/addon-docs": "10.3.5", "@storybook/addon-links": "10.3.5", "@storybook/react-vite": "10.3.5", diff --git a/apps/admin-x-framework/package.json b/apps/admin-x-framework/package.json index c9488651280..4b08fc47de8 100644 --- a/apps/admin-x-framework/package.json +++ b/apps/admin-x-framework/package.json @@ -88,7 +88,6 @@ "glob": "^10.5.0", "jsdom": "28.1.0", "msw": "2.12.14", - "sinon": "18.0.1", "typescript": "5.9.3", "vite": "5.4.21", "vite-plugin-css-injected-by-js": "3.5.2", diff --git a/apps/admin/package.json b/apps/admin/package.json index 159e3e5f95e..d89c14220a3 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -20,7 +20,6 @@ "@tryghost/posts": "workspace:*", "@tryghost/shade": "workspace:*", "@tryghost/stats": "workspace:*", - "lodash": "4.18.1", "mingo": "2.5.3", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/apps/announcement-bar/package.json b/apps/announcement-bar/package.json index 85a88ba34af..5f15ee6a83d 100644 --- a/apps/announcement-bar/package.json +++ b/apps/announcement-bar/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/announcement-bar", - "version": "1.1.18", + "version": "1.1.19", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", @@ -14,7 +14,6 @@ "registry": "https://registry.npmjs.org/" }, "dependencies": { - "@tryghost/content-api": "1.12.6", "react": "17.0.2", "react-dom": "17.0.2" }, diff --git a/apps/comments-ui/package.json b/apps/comments-ui/package.json index 16bb1c35977..6d9689ce034 100644 --- a/apps/comments-ui/package.json +++ b/apps/comments-ui/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/comments-ui", - "version": "1.4.10", + "version": "1.4.11", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", @@ -64,7 +64,6 @@ "@playwright/test": "1.59.1", "@testing-library/jest-dom": "5.17.0", "@testing-library/react": "12.1.5", - "@testing-library/user-event": "14.6.1", "@tryghost/i18n": "workspace:*", "@vitejs/plugin-react": "4.7.0", "@vitest/coverage-v8": "0.34.6", diff --git a/apps/portal/package.json b/apps/portal/package.json index f92b5f1a758..aec4a02e471 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/portal", - "version": "2.68.29", + "version": "2.68.30", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", @@ -109,7 +109,6 @@ ] }, "devDependencies": { - "@babel/eslint-parser": "7.28.4", "@doist/react-interpolate": "2.2.1", "@sentry/react": "7.120.4", "@testing-library/jest-dom": "6.9.1", diff --git a/apps/posts/package.json b/apps/posts/package.json index 1da2fe6a3c2..189b4788942 100644 --- a/apps/posts/package.json +++ b/apps/posts/package.json @@ -55,7 +55,6 @@ "@tryghost/nql-lang": "0.6.4", "@tryghost/shade": "workspace:*", "i18n-iso-countries": "7.14.0", - "moment": "2.24.0", "moment-timezone": "0.5.45", "papaparse": "5.5.3", "react": "18.3.1", diff --git a/apps/shade/package.json b/apps/shade/package.json index 7ebfda2d9ec..59155d40e19 100644 --- a/apps/shade/package.json +++ b/apps/shade/package.json @@ -71,22 +71,17 @@ "preflight.css" ], "devDependencies": { - "@codemirror/lang-html": "6.4.11", - "@radix-ui/react-tooltip": "1.2.8", "@storybook/addon-docs": "10.3.5", "@storybook/addon-links": "10.3.5", "@storybook/react-vite": "10.3.5", "@tailwindcss/postcss": "4.2.1", "@tailwindcss/vite": "4.2.1", "@testing-library/react": "14.3.1", - "@testing-library/react-hooks": "8.0.1", - "@types/lodash-es": "4.17.12", "@types/node": "22.19.17", "@types/react-world-flags": "1.6.0", "@vitejs/plugin-react": "4.7.0", "@vitest/coverage-v8": "^1.6.1", "c8": "10.1.3", - "chai": "4.5.0", "eslint": "catalog:", "eslint-plugin-react-hooks": "4.6.2", "eslint-plugin-react-refresh": "0.4.24", @@ -94,9 +89,7 @@ "eslint-plugin-tailwindcss": "4.0.0-beta.0", "glob": "^10.5.0", "jsdom": "28.1.0", - "lodash-es": "4.18.1", "postcss": "8.5.6", - "sinon": "18.0.1", "storybook": "10.3.5", "tailwindcss": "4.2.1", "tsc-alias": "^1.8.17", @@ -107,9 +100,6 @@ "vitest": "1.6.1" }, "dependencies": { - "@dnd-kit/core": "6.3.1", - "@dnd-kit/sortable": "7.0.2", - "@ebay/nice-modal-react": "1.2.13", "@hookform/resolvers": "5.2.2", "@number-flow/react": "0.5.10", "@radix-ui/react-accordion": "1.2.12", @@ -118,11 +108,9 @@ "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.8", "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.8", "@radix-ui/react-slider": "1.3.6", @@ -132,26 +120,19 @@ "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", - "@sentry/react": "7.120.4", - "@types/color": "4.2.1", "@types/react": "18.3.28", "@types/react-dom": "18.3.7", "@types/validator": "13.15.10", - "@uiw/react-codemirror": "4.25.2", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", "color": "^5.0.3", "lucide-react": "0.577.0", "moment-timezone": "^0.5.48", - "next-themes": "0.4.6", "react": "18.3.1", - "react-colorful": "5.6.1", "react-dom": "18.3.1", "react-dropzone": "14.2.3", "react-hook-form": "7.72.1", - "react-hot-toast": "2.6.0", - "react-select": "5.10.2", "react-world-flags": "1.6.0", "recharts": "2.15.4", "sonner": "2.0.7", diff --git a/e2e/package.json b/e2e/package.json index 65cc03d1779..7a68ad6b539 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -46,7 +46,6 @@ "knex": "3.1.0", "mysql2": "3.18.1", "stripe": "8.222.0", - "ts-node": "10.9.2", "typescript": "5.9.3", "typescript-eslint": "8.58.0" } diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 307ac3fd96b..d9f14fc3253 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -171,8 +171,6 @@ "*.js": "eslint" }, "dependencies": { - "i18n-iso-countries": "7.14.0", - "lru-cache": "6.0.0", "path-browserify": "1.0.1", "webpack": "5.105.4" }, diff --git a/ghost/core/package.json b/ghost/core/package.json index 02033d294db..bf5965a5953 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -88,7 +88,6 @@ "@faker-js/faker": "7.6.0", "@isaacs/ttlcache": "1.4.1", "@sentry/node": "7.120.4", - "@slack/webhook": "7.0.9", "@tryghost/adapter-base-cache": "0.1.23", "@tryghost/admin-api-schema": "4.7.2", "@tryghost/api-framework": "1.0.7", @@ -103,7 +102,6 @@ "@tryghost/errors": "1.3.13", "@tryghost/helpers": "1.1.103", "@tryghost/html-to-plaintext": "1.0.8", - "@tryghost/http-cache-utils": "0.1.25", "@tryghost/i18n": "workspace:*", "@tryghost/image-transform": "1.4.13", "@tryghost/job-manager": "1.0.9", @@ -113,7 +111,6 @@ "@tryghost/kg-default-atoms": "5.2.1", "@tryghost/kg-default-cards": "10.3.1", "@tryghost/kg-default-nodes": "2.1.1", - "@tryghost/kg-default-transforms": "1.3.1", "@tryghost/kg-html-to-lexical": "1.3.1", "@tryghost/kg-lexical-html-renderer": "1.4.1", "@tryghost/kg-markdown-html-renderer": "7.2.1", @@ -154,7 +151,6 @@ "charset": "1.0.1", "cheerio": "0.22.0", "clsx": "2.1.1", - "cluster-key-slot": "1.1.2", "common-tags": "1.8.2", "compression": "1.8.1", "connect-slashes": "1.4.0", @@ -235,8 +231,6 @@ "simple-dom": "1.4.0", "stoppable": "1.1.0", "stripe": "8.222.0", - "superagent": "5.3.1", - "superagent-throttle": "1.0.1", "terser": "5.46.1", "tiny-glob": "0.2.9", "ua-parser-js": "1.0.41", @@ -273,10 +267,7 @@ "c8": "10.1.3", "cli-progress": "3.12.0", "cssnano": "7.1.1", - "detect-indent": "6.1.0", - "detect-newline": "3.1.0", "expect": "29.7.0", - "find-root": "1.1.0", "form-data": "4.0.5", "html-minifier": "4.0.0", "html-validate": "8.29.0", @@ -290,14 +281,12 @@ "nock": "13.5.6", "nodemon": "3.1.14", "papaparse": "5.5.3", - "parse-prometheus-text-format": "1.1.1", "postcss": "8.5.6", "postcss-cli": "11.0.1", "rewire": "9.0.1", "sinon": "18.0.1", "supertest": "6.3.4", "tmp": "0.2.5", - "toml": "3.0.0", "tsx": "4.21.0", "typescript": "5.9.3" }, diff --git a/ghost/parse-email-address/package.json b/ghost/parse-email-address/package.json index 5f3a717b353..ba7c8e16403 100644 --- a/ghost/parse-email-address/package.json +++ b/ghost/parse-email-address/package.json @@ -28,8 +28,6 @@ "@types/node": "25.6.0", "c8": "10.1.3", "mocha": "11.7.5", - "sinon": "21.0.1", - "ts-node": "10.9.2", "tsx": "4.21.0", "typescript": "5.9.3" }, diff --git a/package.json b/package.json index b0d9683307a..2d67f2caa05 100644 --- a/package.json +++ b/package.json @@ -132,17 +132,13 @@ ] }, "devDependencies": { - "@actions/core": "3.0.0", "@playwright/test": "1.59.1", "@secretlint/secretlint-rule-pattern": "12.3.1", "@secretlint/secretlint-rule-preset-recommend": "12.3.1", - "chalk": "4.1.2", - "chokidar": "3.6.0", "eslint": "catalog:", "eslint-plugin-ghost": "3.5.0", "eslint-plugin-react": "7.37.5", "husky": "9.1.7", - "inquirer": "8.2.7", "jsonc-parser": "3.3.1", "knip": "6.12.0", "lint-staged": "16.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c024f44f1d..642077cb53f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,9 +78,6 @@ importers: .: devDependencies: - '@actions/core': - specifier: 3.0.0 - version: 3.0.0 '@playwright/test': specifier: 1.59.1 version: 1.59.1 @@ -90,12 +87,6 @@ importers: '@secretlint/secretlint-rule-preset-recommend': specifier: 12.3.1 version: 12.3.1 - chalk: - specifier: 4.1.2 - version: 4.1.2 - chokidar: - specifier: 3.6.0 - version: 3.6.0 eslint: specifier: 'catalog:' version: 8.57.1 @@ -108,9 +99,6 @@ importers: husky: specifier: 9.1.7 version: 9.1.7 - inquirer: - specifier: 8.2.7 - version: 8.2.7(@types/node@25.6.0) jsonc-parser: specifier: 3.3.1 version: 3.3.1 @@ -190,9 +178,6 @@ importers: '@testing-library/react': specifier: 14.3.1 version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/dompurify': - specifier: 3.2.0 - version: 3.2.0 '@types/jest': specifier: 29.5.14 version: 29.5.14 @@ -208,9 +193,6 @@ importers: tailwindcss: specifier: ^4.2.2 version: 4.2.2 - ts-jest: - specifier: 29.4.9 - version: 29.4.9(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@5.9.3)))(typescript@5.9.3) vite: specifier: 5.4.21 version: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) @@ -241,9 +223,6 @@ importers: '@tryghost/stats': specifier: workspace:* version: link:../stats - lodash: - specifier: 4.18.1 - version: 4.18.1 mingo: specifier: 2.5.3 version: 2.5.3 @@ -580,9 +559,6 @@ importers: msw: specifier: 2.12.14 version: 2.12.14(@types/node@25.6.0)(typescript@5.9.3) - sinon: - specifier: 18.0.1 - version: 18.0.1 typescript: specifier: 5.9.3 version: 5.9.3 @@ -734,9 +710,6 @@ importers: apps/announcement-bar: dependencies: - '@tryghost/content-api': - specifier: 1.12.6 - version: 1.12.6 react: specifier: 17.0.2 version: 17.0.2 @@ -829,9 +802,6 @@ importers: '@testing-library/react': specifier: 12.1.5 version: 12.1.5(@types/react@18.3.28)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@testing-library/user-event': - specifier: 14.6.1 - version: 14.6.1(@testing-library/dom@10.4.0) '@tryghost/i18n': specifier: workspace:* version: link:../../ghost/i18n @@ -899,9 +869,6 @@ importers: specifier: 2.1.0 version: 2.1.0 devDependencies: - '@babel/eslint-parser': - specifier: 7.28.4 - version: 7.28.4(@babel/core@7.29.0)(eslint@8.57.1) '@doist/react-interpolate': specifier: 2.2.1 version: 2.2.1(prop-types@15.8.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -980,9 +947,6 @@ importers: i18n-iso-countries: specifier: 7.14.0 version: 7.14.0 - moment: - specifier: 2.30.1 - version: 2.30.1 moment-timezone: specifier: 0.5.45 version: 0.5.45 @@ -1050,15 +1014,6 @@ importers: apps/shade: dependencies: - '@dnd-kit/core': - specifier: 6.3.1 - version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@dnd-kit/sortable': - specifier: 7.0.2 - version: 7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) - '@ebay/nice-modal-react': - specifier: 1.2.13 - version: 1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@hookform/resolvers': specifier: 5.2.2 version: 5.2.2(react-hook-form@7.72.1(react@18.3.1)) @@ -1083,9 +1038,6 @@ importers: '@radix-ui/react-dropdown-menu': specifier: 2.1.16 version: 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-form': - specifier: 0.1.8 - version: 0.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-hover-card': specifier: 1.1.15 version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1095,9 +1047,6 @@ importers: '@radix-ui/react-popover': specifier: 1.1.15 version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-radio-group': - specifier: 1.3.8 - version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-select': specifier: 2.2.6 version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1125,12 +1074,6 @@ importers: '@radix-ui/react-tooltip': specifier: 1.2.8 version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@sentry/react': - specifier: 7.120.4 - version: 7.120.4(react@18.3.1) - '@types/color': - specifier: 4.2.1 - version: 4.2.1 '@types/react': specifier: 18.3.28 version: 18.3.28 @@ -1140,9 +1083,6 @@ importers: '@types/validator': specifier: 13.15.10 version: 13.15.10 - '@uiw/react-codemirror': - specifier: 4.25.2 - version: 4.25.2(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.40.0)(codemirror@5.65.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -1161,15 +1101,9 @@ importers: moment-timezone: specifier: 0.5.45 version: 0.5.45 - next-themes: - specifier: 0.4.6 - version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 - react-colorful: - specifier: 5.6.1 - version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: specifier: 18.3.1 version: 18.3.1(react@18.3.1) @@ -1179,12 +1113,6 @@ importers: react-hook-form: specifier: 7.72.1 version: 7.72.1(react@18.3.1) - react-hot-toast: - specifier: 2.6.0 - version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-select: - specifier: 5.10.2 - version: 5.10.2(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-world-flags: specifier: 1.6.0 version: 1.6.0(react@18.3.1) @@ -1204,9 +1132,6 @@ importers: specifier: 4.1.12 version: 4.1.12 devDependencies: - '@codemirror/lang-html': - specifier: 6.4.11 - version: 6.4.11 '@storybook/addon-docs': specifier: 10.3.5 version: 10.3.5(@types/react@18.3.28)(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.5(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))(webpack@5.105.4(@swc/core@1.15.21(@swc/helpers@0.5.21))(esbuild@0.27.4)) @@ -1225,12 +1150,6 @@ importers: '@testing-library/react': specifier: 14.3.1 version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@testing-library/react-hooks': - specifier: 8.0.1 - version: 8.0.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/lodash-es': - specifier: 4.17.12 - version: 4.17.12 '@types/node': specifier: 22.19.17 version: 22.19.17 @@ -1246,9 +1165,6 @@ importers: c8: specifier: 10.1.3 version: 10.1.3 - chai: - specifier: 4.5.0 - version: 4.5.0 eslint: specifier: 'catalog:' version: 8.57.1 @@ -1270,15 +1186,9 @@ importers: jsdom: specifier: 28.1.0 version: 28.1.0(@noble/hashes@1.8.0) - lodash-es: - specifier: 4.18.1 - version: 4.18.1 postcss: specifier: 8.5.6 version: 8.5.6 - sinon: - specifier: 18.0.1 - version: 18.0.1 storybook: specifier: 10.3.5 version: 10.3.5(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1582,9 +1492,6 @@ importers: stripe: specifier: 8.222.0 version: 8.222.0 - ts-node: - specifier: 10.9.2 - version: 10.9.2(@swc/core@1.15.21(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -1594,12 +1501,6 @@ importers: ghost/admin: dependencies: - i18n-iso-countries: - specifier: 7.14.0 - version: 7.14.0 - lru-cache: - specifier: 6.0.0 - version: 6.0.0 path-browserify: specifier: 1.0.1 version: 1.0.1 @@ -2003,9 +1904,6 @@ importers: '@sentry/node': specifier: 7.120.4 version: 7.120.4 - '@slack/webhook': - specifier: 7.0.9 - version: 7.0.9 '@tryghost/adapter-base-cache': specifier: 0.1.23 version: 0.1.23 @@ -2048,9 +1946,6 @@ importers: '@tryghost/html-to-plaintext': specifier: 1.0.8 version: 1.0.8 - '@tryghost/http-cache-utils': - specifier: 0.1.25 - version: 0.1.25 '@tryghost/i18n': specifier: workspace:* version: link:../i18n @@ -2078,9 +1973,6 @@ importers: '@tryghost/kg-default-nodes': specifier: 2.1.1 version: 2.1.1(@noble/hashes@1.8.0) - '@tryghost/kg-default-transforms': - specifier: 1.3.1 - version: 1.3.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) '@tryghost/kg-html-to-lexical': specifier: 1.3.1 version: 1.3.1(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) @@ -2201,9 +2093,6 @@ importers: clsx: specifier: 2.1.1 version: 2.1.1 - cluster-key-slot: - specifier: 1.1.2 - version: 1.1.2 common-tags: specifier: 1.8.2 version: 1.8.2 @@ -2444,12 +2333,6 @@ importers: stripe: specifier: 8.222.0 version: 8.222.0 - superagent: - specifier: 5.3.1 - version: 5.3.1 - superagent-throttle: - specifier: 1.0.1 - version: 1.0.1 terser: specifier: 5.46.1 version: 5.46.1 @@ -2532,18 +2415,9 @@ importers: cssnano: specifier: 7.1.1 version: 7.1.1(postcss@8.5.6) - detect-indent: - specifier: 6.1.0 - version: 6.1.0 - detect-newline: - specifier: 3.1.0 - version: 3.1.0 expect: specifier: 29.7.0 version: 29.7.0 - find-root: - specifier: 1.1.0 - version: 1.1.0 html-minifier: specifier: 4.0.0 version: 4.0.0 @@ -2577,9 +2451,6 @@ importers: nodemon: specifier: 3.1.14 version: 3.1.14 - parse-prometheus-text-format: - specifier: 1.1.1 - version: 1.1.1 postcss: specifier: 8.5.6 version: 8.5.6 @@ -2598,9 +2469,6 @@ importers: tmp: specifier: 0.2.5 version: 0.2.5 - toml: - specifier: 3.0.0 - version: 3.0.0 tsx: specifier: 4.21.0 version: 4.21.0 @@ -2652,12 +2520,6 @@ importers: mocha: specifier: 11.7.5 version: 11.7.5 - sinon: - specifier: 21.0.1 - version: 21.0.1 - ts-node: - specifier: 10.9.2 - version: 10.9.2(@swc/core@1.15.21(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@5.9.3) tsx: specifier: 4.21.0 version: 4.21.0 @@ -7016,14 +6878,6 @@ packages: Deprecated: no longer maintained and no longer used by Sinon packages. See https://github.com/sinonjs/nise/issues/243 for replacement details. - '@slack/types@2.20.1': - resolution: {integrity: sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==} - engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} - - '@slack/webhook@7.0.9': - resolution: {integrity: sha512-hMfkQ5Y3Y7FtL+ZYhcxFblidx4Z2LPRFrhY1KJb6NqQdnK6kzTzTS1mjH5taVQIB496eqwpg9FE9mq9BFx0DWw==} - engines: {node: '>= 18', npm: '>= 8.6.0'} - '@smithy/chunked-blob-reader-native@4.2.3': resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} engines: {node: '>=18.0.0'} @@ -8725,9 +8579,6 @@ packages: '@tryghost/config@2.0.3': resolution: {integrity: sha512-hUC+OpeYKr8/5GJjc55KKe6M7r6S9MbnQZUEug572QY9f/iJwK1AwhbQ6rUR0iNOymPbv0MqUiT3lkYiB9nVbg==} - '@tryghost/content-api@1.12.6': - resolution: {integrity: sha512-QCsSG10onLqpXKN7F8yHefOgRBtWdmi30s2dV7BYY5+/Iwgsaf/o/U5oTQhlsGkO3P73Zfw++tnqOL2wO9IYUA==} - '@tryghost/custom-fonts@1.0.8': resolution: {integrity: sha512-56epGhRXXk1QfKYxzyvnm7OCszdlWmMbw9XYq1w5L/67mPoQPX+4NkkNmZqlq/0CjNahzExOB0nOm7aHVDwvWQ==} @@ -9043,22 +8894,13 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/color-convert@2.0.4': - resolution: {integrity: sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==} - '@types/color-convert@3.0.1': resolution: {integrity: sha512-Wj7O4Y7tPo/3y9z4K0XRLbLBP2hCHq2vlmLkR0uQDGmLdoUVDmUrXy50ZffMxZKzBpYxFT+4FnDT8xzMlnKRQQ==} deprecated: This is a stub types definition. color-convert provides its own type definitions, so you do not need this installed. - '@types/color-name@1.1.5': - resolution: {integrity: sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg==} - '@types/color@4.2.0': resolution: {integrity: sha512-6+xrIRImMtGAL2X3qYkd02Mgs+gFGs+WsK0b7VVMaO4mYRISwyTjcqNrO0mNSmYEoq++rSLDB2F5HDNmqfOe+A==} - '@types/color@4.2.1': - resolution: {integrity: sha512-ResWeDLy1vozIMbD6JLRKuNBbIcIlBkjTIxVHHd5Cqtm77T+ahH3BWE/PWv1OhFd1HAwcn8no4ig2uTaRXpYQQ==} - '@types/command-line-args@5.2.3': resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} @@ -9119,10 +8961,6 @@ packages: '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} - '@types/dompurify@3.2.0': - resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} - deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. - '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -11038,10 +10876,6 @@ packages: resolution: {integrity: sha512-rV2tY8amv+2ERYNNC7voCl1A4Mh+s2IvyyDo3DAMKhaR4ME8r+4t9MH0Fgqjpe1ievESYX9Pes7gf05LBBUCRA==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -14406,10 +14240,6 @@ packages: engines: {node: '>=18.3.0'} hasBin: true - formidable@1.2.6: - resolution: {integrity: sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==} - deprecated: 'Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau' - formidable@2.1.5: resolution: {integrity: sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==} @@ -17516,12 +17346,6 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next-themes@0.4.6: - resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} - peerDependencies: - react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -18088,9 +17912,6 @@ packages: resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} engines: {node: '>=0.10.0'} - parse-prometheus-text-format@1.1.1: - resolution: {integrity: sha512-dBlhYVACjRdSqLMFe4/Q1l/Gd3UmXm8ruvsTi7J6ul3ih45AkzkVpI5XHV4aZ37juGZW5+3dGU5lwk+QLM9XJA==} - parse-srcset@1.0.2: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} @@ -20094,9 +19915,6 @@ packages: engines: {node: '>= 0.10'} hasBin: true - shallow-equal@1.2.1: - resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} - sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -20181,9 +19999,6 @@ packages: sinon@18.0.1: resolution: {integrity: sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==} - sinon@21.0.1: - resolution: {integrity: sha512-Z0NVCW45W8Mg5oC/27/+fCqIHFnW8kpkFOq0j9XJIev4Ld0mKmERaZv5DMLAb9fGCevjKwaEeIQz5+MBXfZcDw==} - sinon@21.1.1: resolution: {integrity: sha512-Tn8sLJZay8gRFQycxVwgL86PSUt9SnS9N2wCg12XyR75BssSS1c2fBPFUzwDj7XTNZlaM8+qkETTBYWnkKWXwg==} @@ -20673,14 +20488,6 @@ packages: sum-up@1.0.3: resolution: {integrity: sha512-zw5P8gnhiqokJUWRdR6F4kIIIke0+ubQSGyYUY506GCbJWtV7F6Xuy0j6S125eSX2oF+a8KdivsZ8PlVEH0Mcw==} - superagent-throttle@1.0.1: - resolution: {integrity: sha512-m5Ngf0S5QoA84zgwVqVnVA34u9uvo8uM+QEF9B7eNI5FDaSoSoUwQsx7V1GmLXgYLkolhIiucFDVJXF9z49hgQ==} - - superagent@5.3.1: - resolution: {integrity: sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==} - engines: {node: '>= 7.0.0'} - deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net - superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} @@ -21074,9 +20881,6 @@ packages: resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} engines: {node: '>=10'} - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - tooltip.js@1.3.3: resolution: {integrity: sha512-XWWuy/dBdF/F/YpRE955yqBZ4VdLfiTAUdOqoU+wJm6phJlMpEzl/iYHZ+qJswbeT9VG822bNfsETF9wzmoy5A==} deprecated: Tooltip.js is not supported anymore, please migrate to tippy.js @@ -21161,33 +20965,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-jest@29.4.9: - resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==} - engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 || ^30.0.0 - '@jest/types': ^29.0.0 || ^30.0.0 - babel-jest: ^29.0.0 || ^30.0.0 - esbuild: '*' - jest: ^29.0.0 || ^30.0.0 - jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <7' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/transform': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - jest-util: - optional: true - ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -24167,6 +23944,7 @@ snapshots: '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 + optional: true '@csstools/color-helpers@6.0.2': {} @@ -25556,13 +25334,6 @@ snapshots: optionalDependencies: '@types/node': 22.19.17 - '@inquirer/external-editor@1.0.3(@types/node@25.6.0)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 - optionalDependencies: - '@types/node': 25.6.0 - '@inquirer/figures@1.0.15': {} '@inquirer/type@3.0.10(@types/node@25.6.0)': @@ -25904,6 +25675,7 @@ snapshots: dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + optional: true '@js-sdsl/ordered-map@4.4.2': {} @@ -27776,16 +27548,6 @@ snapshots: '@sinonjs/text-encoding@0.7.3': {} - '@slack/types@2.20.1': {} - - '@slack/webhook@7.0.9': - dependencies: - '@slack/types': 2.20.1 - '@types/node': 25.6.0 - axios: 1.16.0 - transitivePeerDependencies: - - debug - '@smithy/chunked-blob-reader-native@4.2.3': dependencies: '@smithy/util-base64': 4.3.2 @@ -29922,12 +29684,6 @@ snapshots: '@tryghost/root-utils': 2.1.0 nconf: 0.13.0 - '@tryghost/content-api@1.12.6': - dependencies: - axios: 1.16.0 - transitivePeerDependencies: - - debug - '@tryghost/custom-fonts@1.0.8': {} '@tryghost/database-info@0.3.22': {} @@ -30536,13 +30292,17 @@ snapshots: - react-native-b4a - supports-color - '@tsconfig/node10@1.0.12': {} + '@tsconfig/node10@1.0.12': + optional: true - '@tsconfig/node12@1.0.11': {} + '@tsconfig/node12@1.0.11': + optional: true - '@tsconfig/node14@1.0.3': {} + '@tsconfig/node14@1.0.3': + optional: true - '@tsconfig/node16@1.0.4': {} + '@tsconfig/node16@1.0.4': + optional: true '@tybys/wasm-util@0.10.2': dependencies: @@ -30621,24 +30381,14 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/color-convert@2.0.4': - dependencies: - '@types/color-name': 1.1.5 - '@types/color-convert@3.0.1': dependencies: color-convert: 3.1.3 - '@types/color-name@1.1.5': {} - '@types/color@4.2.0': dependencies: '@types/color-convert': 3.0.1 - '@types/color@4.2.1': - dependencies: - '@types/color-convert': 2.0.4 - '@types/command-line-args@5.2.3': {} '@types/command-line-usage@5.0.4': {} @@ -30696,10 +30446,6 @@ snapshots: '@types/doctrine@0.0.9': {} - '@types/dompurify@3.2.0': - dependencies: - dompurify: 3.4.1 - '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -31991,7 +31737,8 @@ snapshots: readable-stream: 3.6.2 optional: true - arg@4.1.3: {} + arg@4.1.3: + optional: true arg@5.0.2: {} @@ -33725,10 +33472,6 @@ snapshots: - sqlite3 - supports-color - bs-logger@0.2.6: - dependencies: - fast-json-stable-stringify: 2.1.0 - bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -34733,7 +34476,8 @@ snapshots: - supports-color - ts-node - create-require@1.1.1: {} + create-require@1.1.1: + optional: true crelt@1.0.6: {} @@ -38379,8 +38123,6 @@ snapshots: dependencies: fd-package-json: 2.0.0 - formidable@1.2.6: {} - formidable@2.1.5: dependencies: '@paralleldrive/cuid2': 2.3.1 @@ -39520,26 +39262,6 @@ snapshots: transitivePeerDependencies: - '@types/node' - inquirer@8.2.7(@types/node@25.6.0): - dependencies: - '@inquirer/external-editor': 1.0.3(@types/node@25.6.0) - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - figures: 3.2.0 - lodash: 4.18.1 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.2 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 6.2.0 - transitivePeerDependencies: - - '@types/node' - install-artifact-from-github@1.4.0: {} internal-slot@1.1.0: @@ -41573,7 +41295,8 @@ snapshots: dependencies: semver: 7.7.4 - make-error@1.3.6: {} + make-error@1.3.6: + optional: true make-fetch-happen@15.0.5: dependencies: @@ -42362,11 +42085,6 @@ snapshots: neo-async@2.6.2: {} - next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - nice-try@1.0.5: {} nise@4.1.0: @@ -43183,10 +42901,6 @@ snapshots: parse-passwd@1.0.0: {} - parse-prometheus-text-format@1.1.1: - dependencies: - shallow-equal: 1.2.1 - parse-srcset@1.0.2: {} parse-static-imports@1.1.0: {} @@ -45503,8 +45217,6 @@ snapshots: safe-buffer: 5.2.1 to-buffer: 1.2.2 - shallow-equal@1.2.1: {} - sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -45635,14 +45347,6 @@ snapshots: nise: 6.1.4 supports-color: 7.2.0 - sinon@21.0.1: - dependencies: - '@sinonjs/commons': 3.0.1 - '@sinonjs/fake-timers': 15.1.1 - '@sinonjs/samsam': 8.0.3 - diff: 8.0.4 - supports-color: 7.2.0 - sinon@21.1.1: dependencies: '@sinonjs/commons': 3.0.1 @@ -46270,24 +45974,6 @@ snapshots: dependencies: chalk: 1.1.3 - superagent-throttle@1.0.1: {} - - superagent@5.3.1: - dependencies: - component-emitter: 1.3.1 - cookiejar: 2.1.4 - debug: 4.4.3(supports-color@5.5.0) - fast-safe-stringify: 2.1.1 - form-data: 3.0.4 - formidable: 1.2.6 - methods: 1.1.2 - mime: 2.6.0 - qs: 6.15.0 - readable-stream: 3.6.2 - semver: 7.7.4 - transitivePeerDependencies: - - supports-color - superagent@8.1.2: dependencies: component-emitter: 1.3.1 @@ -46850,8 +46536,6 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - toml@3.0.0: {} - tooltip.js@1.3.3: dependencies: popper.js: 1.16.1 @@ -46937,26 +46621,6 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.9(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@5.9.3)))(typescript@5.9.3): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.9 - jest: 29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.4 - type-fest: 4.41.0 - typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.29.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 - babel-jest: 29.7.0(@babel/core@7.29.0) - jest-util: 30.3.0 - ts-node@10.9.2(@swc/core@1.15.21(@swc/helpers@0.5.21))(@types/node@22.19.17)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -46997,6 +46661,7 @@ snapshots: yn: 3.1.1 optionalDependencies: '@swc/core': 1.15.21(@swc/helpers@0.5.21) + optional: true tsc-alias@1.8.17: dependencies: @@ -47410,7 +47075,8 @@ snapshots: uuid@9.0.1: {} - v8-compile-cache-lib@3.0.1: {} + v8-compile-cache-lib@3.0.1: + optional: true v8-compile-cache@2.4.0: {} @@ -48381,7 +48047,8 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 - yn@3.1.1: {} + yn@3.1.1: + optional: true yocto-queue@0.1.0: {} From 67d60e8eb9cdf2688ff55b03b58e9dc65013e229 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 6 May 2026 15:42:51 -0500 Subject: [PATCH 08/12] Bumped i18next-parser to 9.4.0 (#27721) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref `i18next-parser@8.13.0` bundles `vue-template-compiler@2.x` as part of its Vue extractor, which carries a moderate client-side XSS advisory (no fix in v2; Vue 2 reached EOL). v9 dropped the Vue extractor from declared deps entirely and also bumped its bundled `esbuild` past the moderate dev-server SSRF advisory range. Cross-major (8 → 9) on a Ghost-owned tooling dep, but `i18next-parser` is a CI/build-time translation extractor — not runtime code, never deployed. The risk surface of bumping it is the build pipeline (`pnpm --filter @tryghost/i18n run translate` and `lint:translations`), not production behavior. ## Behavior change in the lexer v9 replaced its babel-based JS lexer with a TypeScript-compiler-based one. The new lexer is stricter and crashes when fed minified JS bundles — specifically the four `*.min.js` files in `ghost/core/core/frontend/public/` (admin-auth, comment-counts, ghost-stats, member-attribution, private). These are pre-built artifacts that don't contain `t()` calls anyway, so excluding them from `translate:ghost`'s glob with `!../core/core/frontend/public/*.min.js` is safe. This is not a concern given the content of those scripts. --- ghost/i18n/package.json | 4 +- pnpm-lock.yaml | 452 +++++++++++++++++++++------------------- 2 files changed, 239 insertions(+), 217 deletions(-) diff --git a/ghost/i18n/package.json b/ghost/i18n/package.json index 5292ec24179..8eb89b65d6e 100644 --- a/ghost/i18n/package.json +++ b/ghost/i18n/package.json @@ -19,7 +19,7 @@ "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", "lint:translations": "node ./test/i18n.lint.js", "translate": "pnpm translate:ghost && pnpm translate:portal && pnpm translate:signup-form && pnpm translate:comments && pnpm translate:search && node generate-context.js", - "translate:ghost": "NAMESPACE=ghost i18next '../core/core/{frontend,server,shared}/**/*.{js,jsx}' '../core/core/server/services/email-rendering/partials/**/*.hbs' '../core/core/server/services/email-service/email-templates/**/*.hbs' '../core/core/server/services/comments/email-templates/**/*.hbs' '../core/core/server/services/member-welcome-emails/email-templates/**/*.hbs'", + "translate:ghost": "NAMESPACE=ghost i18next '../core/core/{frontend,server,shared}/**/*.{js,jsx}' '!../core/core/frontend/public/*.min.js' '../core/core/server/services/email-rendering/partials/**/*.hbs' '../core/core/server/services/email-service/email-templates/**/*.hbs' '../core/core/server/services/comments/email-templates/**/*.hbs' '../core/core/server/services/member-welcome-emails/email-templates/**/*.hbs'", "translate:portal": "NAMESPACE=portal i18next '../../apps/portal/src/**/*.{js,jsx}'", "translate:signup-form": "NAMESPACE=signup-form i18next '../../apps/signup-form/src/**/*.{ts,tsx}'", "translate:comments": "NAMESPACE=comments i18next '../../apps/comments-ui/src/**/*.{ts,tsx}'", @@ -33,7 +33,7 @@ "devDependencies": { "c8": "10.1.3", "glob": "^13.0.6", - "i18next-parser": "8.13.0", + "i18next-parser": "9.3.0", "mocha": "11.7.5" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 642077cb53f..d80bfeb2b73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2499,8 +2499,8 @@ importers: specifier: ^13.0.6 version: 13.0.6 i18next-parser: - specifier: 8.13.0 - version: 8.13.0 + specifier: 9.3.0 + version: 9.3.0 mocha: specifier: 11.7.5 version: 11.7.5 @@ -4033,15 +4033,15 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} - '@esbuild/aix-ppc64@0.20.2': - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -4051,15 +4051,15 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.20.2': - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -4069,15 +4069,15 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.20.2': - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} cpu: [arm] os: [android] @@ -4087,15 +4087,15 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.20.2': - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} cpu: [x64] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} cpu: [x64] os: [android] @@ -4105,15 +4105,15 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.20.2': - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -4123,15 +4123,15 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.20.2': - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -4141,15 +4141,15 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.20.2': - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -4159,15 +4159,15 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.20.2': - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -4177,15 +4177,15 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.20.2': - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -4195,15 +4195,15 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.20.2': - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -4213,15 +4213,15 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.20.2': - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -4231,15 +4231,15 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.20.2': - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -4249,15 +4249,15 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.20.2': - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -4267,15 +4267,15 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.20.2': - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -4285,15 +4285,15 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.20.2': - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -4303,15 +4303,15 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.20.2': - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -4321,15 +4321,15 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.20.2': - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -4339,21 +4339,27 @@ packages: cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.27.4': resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.20.2': - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -4363,21 +4369,27 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.27.4': resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.20.2': - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -4387,21 +4399,27 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/openharmony-arm64@0.27.4': resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.20.2': - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -4411,15 +4429,15 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.20.2': - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -4429,15 +4447,15 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.20.2': - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -4447,15 +4465,15 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.20.2': - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -11472,6 +11490,10 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -12368,9 +12390,6 @@ packages: resolution: {integrity: sha512-/9+C44X7lot0IeiyfgJmETtRMhBidBYM2QFFIkGa0U1k+hSyY87Nw7PY3eDqpvCBm7I3WCSfPeZskW/YYq6m4g==} engines: {node: '>=4'} - de-indent@1.0.2: - resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} - debug-logfmt@1.4.10: resolution: {integrity: sha512-+8rNw7zjXNRntMoJyp5211Y4W3nkhCCMBO7qe8Pht/9NscMklHwyTXMLUzk84YUDSksg87XRmK/LCzJdJ4eU7Q==} engines: {node: '>= 8'} @@ -13463,16 +13482,16 @@ packages: es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} - esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} hasBin: true + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.27.4: resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} engines: {node: '>=18'} @@ -14971,9 +14990,9 @@ packages: resolution: {integrity: sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==} engines: {node: '>= 12'} - i18next-parser@8.13.0: - resolution: {integrity: sha512-XU7resoeNcpJazh29OncQQUH6HsgCxk06RqBBDAmLHldafxopfCHY1vElyG/o3EY0Sn7XjelAmPTV0SgddJEww==} - engines: {node: '>=16.0.0 || >=18.0.0 || >=20.0.0', npm: '>=6', yarn: '>=1'} + i18next-parser@9.3.0: + resolution: {integrity: sha512-VaQqk/6nLzTFx1MDiCZFtzZXKKyBV6Dv0cJMFM/hOt4/BWHWRgYafzYfVQRUzotwUwjqeNCprWnutzD/YAGczg==} + engines: {node: ^18.0.0 || ^20.0.0 || ^22.0.0, npm: '>=6', yarn: '>=1'} deprecated: Project is deprecated, use i18next-cli instead hasBin: true @@ -21709,9 +21728,6 @@ packages: vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} - vue-template-compiler@2.7.16: - resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} - w3c-hr-time@1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} deprecated: Use your platform's native performance.now() and performance.timeOrigin. @@ -24658,219 +24674,228 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} - '@esbuild/aix-ppc64@0.20.2': + '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.21.5': + '@esbuild/aix-ppc64@0.25.12': optional: true '@esbuild/aix-ppc64@0.27.4': optional: true - '@esbuild/android-arm64@0.20.2': + '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.21.5': + '@esbuild/android-arm64@0.25.12': optional: true '@esbuild/android-arm64@0.27.4': optional: true - '@esbuild/android-arm@0.20.2': + '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.21.5': + '@esbuild/android-arm@0.25.12': optional: true '@esbuild/android-arm@0.27.4': optional: true - '@esbuild/android-x64@0.20.2': + '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.21.5': + '@esbuild/android-x64@0.25.12': optional: true '@esbuild/android-x64@0.27.4': optional: true - '@esbuild/darwin-arm64@0.20.2': + '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.21.5': + '@esbuild/darwin-arm64@0.25.12': optional: true '@esbuild/darwin-arm64@0.27.4': optional: true - '@esbuild/darwin-x64@0.20.2': + '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.21.5': + '@esbuild/darwin-x64@0.25.12': optional: true '@esbuild/darwin-x64@0.27.4': optional: true - '@esbuild/freebsd-arm64@0.20.2': + '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.21.5': + '@esbuild/freebsd-arm64@0.25.12': optional: true '@esbuild/freebsd-arm64@0.27.4': optional: true - '@esbuild/freebsd-x64@0.20.2': + '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.21.5': + '@esbuild/freebsd-x64@0.25.12': optional: true '@esbuild/freebsd-x64@0.27.4': optional: true - '@esbuild/linux-arm64@0.20.2': + '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.21.5': + '@esbuild/linux-arm64@0.25.12': optional: true '@esbuild/linux-arm64@0.27.4': optional: true - '@esbuild/linux-arm@0.20.2': + '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.21.5': + '@esbuild/linux-arm@0.25.12': optional: true '@esbuild/linux-arm@0.27.4': optional: true - '@esbuild/linux-ia32@0.20.2': + '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.21.5': + '@esbuild/linux-ia32@0.25.12': optional: true '@esbuild/linux-ia32@0.27.4': optional: true - '@esbuild/linux-loong64@0.20.2': + '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.21.5': + '@esbuild/linux-loong64@0.25.12': optional: true '@esbuild/linux-loong64@0.27.4': optional: true - '@esbuild/linux-mips64el@0.20.2': + '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.21.5': + '@esbuild/linux-mips64el@0.25.12': optional: true '@esbuild/linux-mips64el@0.27.4': optional: true - '@esbuild/linux-ppc64@0.20.2': + '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.21.5': + '@esbuild/linux-ppc64@0.25.12': optional: true '@esbuild/linux-ppc64@0.27.4': optional: true - '@esbuild/linux-riscv64@0.20.2': + '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.21.5': + '@esbuild/linux-riscv64@0.25.12': optional: true '@esbuild/linux-riscv64@0.27.4': optional: true - '@esbuild/linux-s390x@0.20.2': + '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.21.5': + '@esbuild/linux-s390x@0.25.12': optional: true '@esbuild/linux-s390x@0.27.4': optional: true - '@esbuild/linux-x64@0.20.2': + '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/linux-x64@0.21.5': + '@esbuild/linux-x64@0.25.12': optional: true '@esbuild/linux-x64@0.27.4': optional: true - '@esbuild/netbsd-arm64@0.27.4': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.20.2': + '@esbuild/netbsd-arm64@0.27.4': optional: true '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.27.4': optional: true - '@esbuild/openbsd-arm64@0.27.4': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.20.2': + '@esbuild/openbsd-arm64@0.27.4': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.27.4': optional: true - '@esbuild/openharmony-arm64@0.27.4': + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/sunos-x64@0.20.2': + '@esbuild/openharmony-arm64@0.27.4': optional: true '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.27.4': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/win32-arm64@0.20.2': + '@esbuild/sunos-x64@0.27.4': optional: true '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.27.4': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-ia32@0.20.2': + '@esbuild/win32-arm64@0.27.4': optional: true '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.27.4': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-x64@0.20.2': + '@esbuild/win32-ia32@0.27.4': optional: true '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.25.12': + optional: true + '@esbuild/win32-x64@0.27.4': optional: true @@ -34158,6 +34183,8 @@ snapshots: commander@11.1.0: {} + commander@12.1.0: {} + commander@14.0.3: {} commander@2.20.3: {} @@ -34877,8 +34904,6 @@ snapshots: dependencies: time-zone: 1.0.0 - de-indent@1.0.2: {} - debug-logfmt@1.4.10: dependencies: '@kikobeats/time-span': 1.0.12 @@ -36944,32 +36969,6 @@ snapshots: es6-promise@4.2.8: {} - esbuild@0.20.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 - esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -36996,6 +36995,35 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.4: optionalDependencies: '@esbuild/aix-ppc64': 0.27.4 @@ -39070,15 +39098,15 @@ snapshots: dependencies: diacritics: 1.3.0 - i18next-parser@8.13.0: + i18next-parser@9.3.0: dependencies: '@babel/runtime': 7.29.2 broccoli-plugin: 4.0.7 - cheerio: 1.0.0-rc.12 + cheerio: 1.2.0 colors: 1.4.0 - commander: 11.1.0 + commander: 12.1.0 eol: 0.9.1 - esbuild: 0.20.2 + esbuild: 0.25.12 fs-extra: 11.3.4 gulp-sort: 2.0.0 i18next: 23.16.8 @@ -39089,7 +39117,6 @@ snapshots: typescript: 5.9.3 vinyl: 3.0.1 vinyl-fs: 4.0.2 - vue-template-compiler: 2.7.16 transitivePeerDependencies: - bare-abort-controller - react-native-b4a @@ -47574,11 +47601,6 @@ snapshots: vm-browserify@1.1.2: {} - vue-template-compiler@2.7.16: - dependencies: - de-indent: 1.0.2 - he: 1.2.0 - w3c-hr-time@1.0.2: dependencies: browser-process-hrtime: 1.0.0 From 986f78ec062eef13c25225ea091407eaa53486a9 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Wed, 6 May 2026 16:26:43 -0500 Subject: [PATCH 09/12] Made request ID middleware more realistic (#27722) no ref Let's test this middleware without stubbing. --- .../web/parent/middleware/request-id.test.js | 58 +++++++------------ 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/ghost/core/test/unit/server/web/parent/middleware/request-id.test.js b/ghost/core/test/unit/server/web/parent/middleware/request-id.test.js index 4f34d9afe5d..6412b2d7c0a 100644 --- a/ghost/core/test/unit/server/web/parent/middleware/request-id.test.js +++ b/ghost/core/test/unit/server/web/parent/middleware/request-id.test.js @@ -1,50 +1,36 @@ const assert = require('node:assert/strict'); -const {assertExists} = require('../../../../../utils/assertions'); -const sinon = require('sinon'); +const express = require('express'); +const request = require('supertest'); const validator = require('@tryghost/validator'); const requestId = require('../../../../../../core/server/web/parent/middleware/request-id'); describe('Request ID middleware', function () { - let res; - let req; - let next; - - beforeEach(function () { - req = { - get: sinon.stub() - }; - res = { - redirect: sinon.spy(), - set: sinon.spy() - }; - - next = sinon.spy(); + const app = express(); + app.use(requestId); + app.get('/', (req, res) => { + res.json({requestId: req.requestId}); }); - afterEach(function () { - sinon.restore(); + it('generates a new request ID if X-Request-ID not present', async function () { + const {headers, body} = await request(app).get('/'); + assert(!('x-request-id' in headers)); + assert(validator.isUUID(body.requestId)); }); - it('generates a new request ID if X-Request-ID not present', function () { - assert.equal(req.requestId, undefined); - - requestId(req, res, next); - - assertExists(req.requestId); - assert.equal(validator.isUUID(req.requestId), true); - sinon.assert.notCalled(res.set); + it('generates a new request ID if X-Request-ID is an empty string', async function () { + const {headers, body} = await request(app) + .get('/') + .set('X-Request-ID', ''); + assert(!('x-request-id' in headers)); + assert(validator.isUUID(body.requestId)); }); - it('keeps the request ID if X-Request-ID is present', function () { - assert.equal(req.requestId, undefined); - req.get.withArgs('X-Request-ID').returns('abcd'); - - requestId(req, res, next); - - assertExists(req.requestId); - assert.equal(req.requestId, 'abcd'); - sinon.assert.calledOnce(res.set); - sinon.assert.calledWith(res.set, 'X-Request-ID', 'abcd'); + it('keeps the request ID if X-Request-ID is present', async function () { + await request(app) + .get('/') + .set('X-Request-ID', 'abcd') + .expect('X-Request-ID', 'abcd') + .expect({requestId: 'abcd'}); }); }); From d1b8f77f6a682c1aa7e3807c6b9f12ca6ac5d5e4 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Wed, 6 May 2026 17:01:20 -0500 Subject: [PATCH 10/12] Made locals middleware test more realistic (#27725) ref 986f78ec062eef13c25225ea091407eaa53486a9 Let's test this middleware without stubbing. --- .../parent/middleware/ghost-locals.test.js | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/ghost/core/test/unit/server/web/parent/middleware/ghost-locals.test.js b/ghost/core/test/unit/server/web/parent/middleware/ghost-locals.test.js index 3792d66c6ab..9e624177532 100644 --- a/ghost/core/test/unit/server/web/parent/middleware/ghost-locals.test.js +++ b/ghost/core/test/unit/server/web/parent/middleware/ghost-locals.test.js @@ -1,35 +1,23 @@ const assert = require('node:assert/strict'); const {assertExists} = require('../../../../../utils/assertions'); -const _ = require('lodash'); -const sinon = require('sinon'); +const express = require('express'); +const request = require('supertest'); const ghostLocals = require('../../../../../../core/server/web/parent/middleware/ghost-locals'); describe('Theme Handler', function () { - let req; - let res; - let next; - - beforeEach(function () { - req = sinon.spy(); - res = sinon.spy(); - next = sinon.spy(); - }); - - afterEach(function () { - sinon.restore(); + const app = express(); + app.use(ghostLocals); + app.get('/awesome-post', (_req, res) => { + res.json(res.locals); }); describe('ghostLocals', function () { - it('sets all locals', function () { - req.path = '/awesome-post'; - - ghostLocals(req, res, next); - - assert(_.isPlainObject(res.locals)); - assertExists(res.locals.version); - assertExists(res.locals.safeVersion); - assert.equal(res.locals.relativeUrl, req.path); - sinon.assert.called(next); + it('sets all locals', async function () { + const {body} = await request(app) + .get('/awesome-post'); + assertExists(body.version); + assertExists(body.safeVersion); + assert.equal(body.relativeUrl, '/awesome-post'); }); }); }); From 8fe39de3345e9b653c68019fb7e4cc98739c7c72 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Wed, 6 May 2026 17:06:06 -0500 Subject: [PATCH 11/12] Made queue request middleware tests more realistic (#27728) ref 986f78ec062eef13c25225ea091407eaa53486a9 Let's test this middleware without stubbing. --- .../parent/middleware/queue-request.test.js | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/ghost/core/test/unit/server/web/parent/middleware/queue-request.test.js b/ghost/core/test/unit/server/web/parent/middleware/queue-request.test.js index 588aa03916b..aff2c543d96 100644 --- a/ghost/core/test/unit/server/web/parent/middleware/queue-request.test.js +++ b/ghost/core/test/unit/server/web/parent/middleware/queue-request.test.js @@ -1,20 +1,21 @@ -const assert = require('node:assert'); +const assert = require('node:assert/strict'); +const express = require('express'); const sinon = require('sinon'); +const request = require('supertest'); const queueRequest = require('../../../../../../core/server/web/parent/middleware/queue-request'); describe('Queue request middleware', function () { - let req, res, next, config, queueFactory, queue; + let config, queueFactory, queue; beforeEach(function () { - req = {}; - res = {}; - next = sinon.stub(); config = { concurrencyLimit: 123 }; - queue = sinon.stub(); + queue = sinon.stub().callsFake((req, res, next) => { + return next(); + }); queue.queue = { on: sinon.stub(), getLength: sinon.stub().returns(0) @@ -23,6 +24,17 @@ describe('Queue request middleware', function () { queueFactory = sinon.stub().returns(queue); }); + function createApp() { + const app = express(); + + app.use(queueRequest(config, queueFactory)); + app.get(['/foo/bar', '/foo/bar.css'], (req, res) => { + res.json({queueDepth: req.queueDepth}); + }); + + return app; + } + it('should configure the queue using the concurrency limit defined in the config', function () { queueRequest(config, queueFactory); @@ -39,40 +51,36 @@ describe('Queue request middleware', function () { }, /concurrencyLimit must be defined when using queueRequest middleware/, 'error should be thrown'); }); - it('should not queue requests for static assets', function () { - req.path = '/foo/bar.css'; // Assume any path with a file extension is a static asset - - const mw = queueRequest(config, queueFactory); - - mw(req, res, next); + it('should not queue requests for static assets', async function () { + await request(createApp()) + .get('/foo/bar.css') + .expect(200) + .expect({queueDepth: 0}); - assert(next.calledOnce, 'next should be called once'); - assert.equal(queue.calledOnce, 0, 'queue should not be called'); + assert.equal(queue.callCount, 0, 'queue should not be called'); }); - it('should queue the request', function () { - req.path = '/foo/bar'; + it('should queue the request', async function () { + await request(createApp()) + .get('/foo/bar') + .expect(200); - const mw = queueRequest(config, queueFactory); - - mw(req, res, next); - - assert(queue.calledOnce, 'queue should be called once'); - sinon.assert.calledWith(queue, req, res, next); + sinon.assert.calledOnce(queue); + assert.equal(queue.getCall(0).args[0].path, '/foo/bar'); + assert.equal(typeof queue.getCall(0).args[1].json, 'function'); + assert.equal(typeof queue.getCall(0).args[2], 'function'); }); - it('should record the queue depth on a request', function () { + it('should record the queue depth on a request', async function () { const queueLength = 123; queue.queue.getLength.returns(queueLength); - req.path = '/foo/bar'; - - const mw = queueRequest(config, queueFactory); - - mw(req, res, next); + await request(createApp()) + .get('/foo/bar') + .expect(200) + .expect({queueDepth: queueLength}); - assert(queue.queue.getLength, 'queue should be called once'); - assert(req.queueDepth === queueLength, 'queue depth should be set on the request'); + sinon.assert.calledOnce(queue.queue.getLength); }); }); From dffa02b9a83364b40d8a83a5f80e4e9b25bac4b1 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Wed, 6 May 2026 17:09:42 -0500 Subject: [PATCH 12/12] Remove TODO from request ID middleware (#27723) no ref I don't think we intend to do this. Let's remove the TODO. --- ghost/core/core/server/web/parent/middleware/request-id.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/ghost/core/core/server/web/parent/middleware/request-id.js b/ghost/core/core/server/web/parent/middleware/request-id.js index 152b3b50928..4f5c4c39810 100644 --- a/ghost/core/core/server/web/parent/middleware/request-id.js +++ b/ghost/core/core/server/web/parent/middleware/request-id.js @@ -1,8 +1,6 @@ const crypto = require('crypto'); /** - * @TODO: move this middleware to Framework monorepo? - * * @param {import('express').Request} req * @param {import('express').Response} res * @param {import('express').NextFunction} next