[AI] Add knip to detect unused files, dependencies and exports (#8309)

Adds the knip tool with a monorepo-tuned config (knip.json) and a CI gate
in check.yml, plus the dead-code and dependency cleanup it surfaced. The
unused-export/type/duplicate rules are disabled so exporting an unused
symbol is allowed.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matiss Janis Aboltins
2026-06-22 23:11:48 +01:00
committed by GitHub
parent df45b50412
commit c4acaa2951
25 changed files with 666 additions and 934 deletions

View File

@@ -55,6 +55,21 @@ jobs:
download-translations: 'false'
- name: Lint
run: yarn lint
knip:
needs: setup
runs-on: depot-ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Build plugins service
run: yarn build:plugins-service
- name: Check for unused files, dependencies and exports
run: yarn knip
typecheck:
needs: setup
runs-on: depot-ubuntu-latest

93
knip.json Normal file
View File

@@ -0,0 +1,93 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"rules": {
"exports": "off",
"types": "off",
"nsExports": "off",
"nsTypes": "off",
"duplicates": "off"
},
"ignoreBinaries": ["electron-rebuild"],
"workspaces": {
".": {
"entry": ["bin/*.mts"],
"ignore": ["lage.config.js", ".claude/**"]
},
"packages/api": {
"entry": ["index.browser.ts", "browser-worker.ts", "e2e/**/*.{ts,mjs}"],
"ignore": ["typings.ts"],
"ignoreDependencies": [
"@actual-app/crdt",
"better-sqlite3",
"uuid",
"@jlongster/sql.js"
]
},
"packages/ci-actions": {
"entry": ["bin/*.ts"],
"ignoreBinaries": ["bin/tsx"],
"ignoreDependencies": ["extensionless"]
},
"packages/component-library": {
"entry": ["src/icons/index-template.ts", "src/icons/template.ts"],
"ignore": ["src/icons/.svgrrc.js", "src/icons/add-attribute.ts"],
"ignoreDependencies": ["@svgr/babel-plugin-add-jsx-attribute"]
},
"packages/crdt": {
"ignore": ["src/proto/**"],
"ignoreDependencies": ["@bufbuild/protoc-gen-es"]
},
"packages/desktop-client": {
"entry": [
"bin/*.mjs",
"globals.ts",
"src/globals.ts",
"src/build-shims.js"
],
"ignoreDependencies": ["plugins-service"]
},
"packages/desktop-electron": {
"entry": [
"preload.ts",
"server.ts",
"afterSignHook.ts",
"beforePackHook.ts"
],
"ignoreBinaries": ["bin/update-client"],
"ignoreDependencies": ["better-sqlite3"]
},
"packages/docs": {
"entry": [
"src/theme/**/*.{ts,tsx,js,jsx}",
"src/components/**/*.{ts,tsx,js,jsx}",
"docs/**/*.{js,jsx}"
],
"ignore": ["docusaurus.netlify-config.js"]
},
"packages/loot-core": {
"entry": ["src/**/index-types.ts", "src/mocks/**"],
"ignoreDependencies": [
"assert",
"browserify-zlib",
"buffer",
"crypto-browserify",
"path-browserify",
"stream-browserify",
"timers-browserify",
"util"
]
},
"packages/plugins-service": {
"entry": ["src/plugin-service-worker.ts"]
},
"packages/sync-server": {
"entry": [
"app.ts",
"src/app.ts",
"bin/**/*.js",
"src/scripts/**/*.js",
"src/app-*/app-*.{ts,js}"
]
}
}
}

View File

@@ -56,6 +56,7 @@
"rebuild-node": "yarn workspace @actual-app/core rebuild",
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",
"knip": "knip",
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
"constraints": "yarn constraints",
"typecheck": "tsgo -p tsconfig.root.json --noEmit && lage typecheck",
@@ -74,6 +75,7 @@
"eslint-plugin-perfectionist": "^5.9.0",
"eslint-plugin-typescript-paths": "^0.0.33",
"husky": "^9.1.7",
"knip": "^6.17.1",
"lage": "^2.15.13",
"minimatch": "^10.2.5",
"nano-staged": "^1.0.2",

View File

@@ -61,6 +61,8 @@
"@storybook/react-vite": "^10.4.4",
"@svgr/babel-plugin-add-jsx-attribute": "^8.0.0",
"@svgr/cli": "^8.1.0",
"@svgr/core": "^8.1.0",
"@testing-library/react": "16.3.2",
"@types/react": "^19.2.17",
"@typescript/native-preview": "beta",
"@vitejs/plugin-react": "^6.0.2",

View File

@@ -1,109 +0,0 @@
import { useMemo, useReducer, useRef, useState } from 'react';
import { SpaceBetween } from '@actual-app/components/space-between';
import type { CSSProperties } from '@actual-app/components/styles';
import { firstDayOfMonth } from '@actual-app/core/shared/months';
import type {
CategoryGroupEntity,
ScheduleEntity,
} from '@actual-app/core/types/models';
import type { Template } from '@actual-app/core/types/models/templates';
import { useEffectAfterMount } from '#hooks/useEffectAfterMount';
import { BudgetAutomationEditor } from './BudgetAutomationEditor';
import { BudgetAutomationReadOnly } from './BudgetAutomationReadOnly';
import type { DisplayTemplateType } from './constants';
import { DEFAULT_PRIORITY, getInitialState, templateReducer } from './reducer';
type BudgetAutomationProps = {
categories: CategoryGroupEntity[];
schedules: readonly ScheduleEntity[];
template?: Template;
onSave?: (template: Template, displayType: DisplayTemplateType) => void;
onDelete?: () => void;
style?: CSSProperties;
readOnlyStyle?: CSSProperties;
inline?: boolean;
hasLimitAutomation?: boolean;
onAddLimitAutomation?: () => void;
};
const DEFAULT_TEMPLATE: Template = {
directive: 'template',
type: 'periodic',
amount: 0,
period: {
period: 'month',
amount: 1,
},
starting: firstDayOfMonth(new Date()),
priority: DEFAULT_PRIORITY,
};
export const BudgetAutomation = ({
onDelete,
onSave,
categories,
schedules,
readOnlyStyle,
style,
template,
inline = false,
hasLimitAutomation,
onAddLimitAutomation,
}: BudgetAutomationProps) => {
const [isEditing, setIsEditing] = useState(false);
const [state, dispatch] = useReducer(templateReducer, null, () =>
getInitialState(template ?? DEFAULT_TEMPLATE),
);
const onSaveRef = useRef(onSave);
onSaveRef.current = onSave;
useEffectAfterMount(() => {
onSaveRef.current?.(state.template, state.displayType);
}, [state]);
const categoryNameMap = useMemo(() => {
return categories.reduce(
(acc, group) => {
for (const category of group.categories ?? []) {
acc[category.id] = category.name;
}
return acc;
},
{} as Record<string, string>,
);
}, [categories]);
return (
<SpaceBetween
direction="vertical"
align="stretch"
gap={inline ? 0 : 5}
style={{ ...style, minHeight: 'fit-content' }}
>
<BudgetAutomationReadOnly
state={state}
categoryNameMap={categoryNameMap}
isEditing={isEditing}
setIsEditing={setIsEditing}
onDelete={onDelete}
style={readOnlyStyle}
inline={inline}
/>
{isEditing && (
<BudgetAutomationEditor
inline={inline}
state={state}
dispatch={dispatch}
schedules={schedules}
categories={categories}
hasLimitAutomation={hasLimitAutomation}
onAddLimitAutomation={onAddLimitAutomation}
/>
)}
</SpaceBetween>
);
};

View File

@@ -1,229 +0,0 @@
import type { ReactNode } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { InitialFocus } from '@actual-app/components/initial-focus';
import { Select } from '@actual-app/components/select';
import { SpaceBetween } from '@actual-app/components/space-between';
import { styles } from '@actual-app/components/styles';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import type {
CategoryGroupEntity,
ScheduleEntity,
} from '@actual-app/core/types/models';
import { FormField, FormLabel, FormTextLabel } from '#components/forms';
import { setType } from './actions';
import type { Action } from './actions';
import type { ReducerState } from './constants';
import { displayTemplateTypes } from './constants';
import { getDisplayTemplateMeta } from './displayTemplateMeta';
import { BySaveAutomation } from './editor/BySaveAutomation';
import { FixedAutomation } from './editor/FixedAutomation';
import { HistoricalAutomation } from './editor/HistoricalAutomation';
import { LimitAutomation } from './editor/LimitAutomation';
import { LongTermGoalAutomation } from './editor/LongTermGoalAutomation';
import { PercentageAutomation } from './editor/PercentageAutomation';
import { RefillAutomation } from './editor/RefillAutomation';
import { RemainderAutomation } from './editor/RemainderAutomation';
import { ScheduleAutomation } from './editor/ScheduleAutomation';
type BudgetAutomationEditorProps = {
inline: boolean;
state: ReducerState;
dispatch: (action: Action) => void;
schedules: readonly ScheduleEntity[];
categories: CategoryGroupEntity[];
hasLimitAutomation?: boolean;
onAddLimitAutomation?: () => void;
};
const displayTypeToDescription = {
limit: (
<Trans>
Set a cap for all budget contributions to this category across all
automations. The maximum can be set on a monthly, weekly, or daily basis.
For example, a $100 weekly cap would result in a $400 monthly cap ($500
depending on the month).
</Trans>
),
refill: (
<Trans>
Refill the category up to the balance limit set by the balance limit
automation.
</Trans>
),
fixed: (
<Trans>
Add a fixed amount to this category for each week in the month. For
example, $100 per week would be $400 per month in a 4-week month.
</Trans>
),
schedule: (
<Trans>
Add enough to this category to cover the selected schedule. If the
schedule occurs multiple times in a month, an amount will be added for
each occurrence. You can choose to save up for the next occurrence over
time (e.g. save $100 each month for a $300 quarterly bill) or cover each
occurrence when it occurs (e.g. only add the $300 when the bill is due).
</Trans>
),
percentage: (
<Trans>
Add a fixed percentage of your income to this category each month. You can
choose to take the percentage from the current month or the previous
month.
</Trans>
),
historical: (
<Trans>
Add an amount to this category each month based on the values from
previous months. For example, you can copy the amount from a year ago to
budget for an annual expense, or budget the average of the last 3 months
to account for seasonal changes.
</Trans>
),
by: (
<Trans>
Spread a target amount across the months between now and a target date.
Useful for annual goals or saving toward a one-off expense.
</Trans>
),
remainder: (
<Trans>
Split any remaining To Budget across categories using this automation.
Higher weights take a larger share of the leftover funds.
</Trans>
),
goal: (
<Trans>
Set a long-term savings target. This changes the coloring of the balance
on the budget page to be based on progress towards the target rather than
the current month funding progress.
</Trans>
),
};
export function BudgetAutomationEditor({
inline,
state,
dispatch,
schedules,
categories,
hasLimitAutomation = false,
onAddLimitAutomation,
}: BudgetAutomationEditorProps) {
const { t } = useTranslation();
let automationEditor: ReactNode;
switch (state.displayType) {
case 'limit':
automationEditor = (
<LimitAutomation template={state.template} dispatch={dispatch} />
);
break;
case 'refill':
automationEditor = (
<RefillAutomation
hasLimitAutomation={hasLimitAutomation}
onAddLimitAutomation={onAddLimitAutomation}
/>
);
break;
case 'fixed':
automationEditor = (
<FixedAutomation template={state.template} dispatch={dispatch} />
);
break;
case 'schedule':
automationEditor = (
<ScheduleAutomation
schedules={schedules}
template={state.template}
dispatch={dispatch}
/>
);
break;
case 'percentage':
automationEditor = (
<PercentageAutomation
dispatch={dispatch}
template={state.template}
categories={categories}
/>
);
break;
case 'historical':
automationEditor = (
<HistoricalAutomation template={state.template} dispatch={dispatch} />
);
break;
case 'by':
automationEditor = (
<BySaveAutomation template={state.template} dispatch={dispatch} />
);
break;
case 'remainder':
automationEditor = (
<RemainderAutomation template={state.template} dispatch={dispatch} />
);
break;
case 'goal':
automationEditor = (
<LongTermGoalAutomation template={state.template} dispatch={dispatch} />
);
break;
default:
state satisfies never;
automationEditor = (
<Text>
<Trans>Unrecognized automation type.</Trans>
</Text>
);
}
return (
<SpaceBetween
direction="vertical"
align="stretch"
gap={10}
style={{
flex: 1,
...styles.editorPill,
backgroundColor: theme.pillBackgroundLight,
...(inline ? { borderRadius: 0 } : {}),
padding: 30,
minHeight: 'fit-content',
}}
>
<SpaceBetween gap={20} style={{ alignItems: 'flex-start' }}>
<FormField style={{ flexShrink: 0 }}>
<FormLabel title={t('Type')} htmlFor="type-field" />
<InitialFocus>
<Select
id="type-field"
options={displayTemplateTypes.map(type => [
type,
getDisplayTemplateMeta(type).label,
])}
defaultLabel={t('Select an option')}
value={state.displayType}
onChange={type => type && dispatch(setType(type))}
style={{ width: 172 }}
/>
</InitialFocus>
</FormField>
<FormField style={{ flex: 1 }}>
<FormTextLabel title={t('Description')} />
<Text>
{displayTypeToDescription[state.displayType] ?? (
<Trans>No description available</Trans>
)}
</Text>
</FormField>
</SpaceBetween>
{automationEditor}
</SpaceBetween>
);
}

View File

@@ -1,144 +0,0 @@
import type { Dispatch, SetStateAction } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Button } from '@actual-app/components/button';
import { SvgDelete } from '@actual-app/components/icons/v0';
import {
SvgCheveronDown,
SvgCheveronUp,
} from '@actual-app/components/icons/v1';
import { SpaceBetween } from '@actual-app/components/space-between';
import type { CSSProperties } from '@actual-app/components/styles';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import type { ReducerState } from './constants';
import { BySaveAutomationReadOnly } from './editor/BySaveAutomationReadOnly';
import { FixedAutomationReadOnly } from './editor/FixedAutomationReadOnly';
import { HistoricalAutomationReadOnly } from './editor/HistoricalAutomationReadOnly';
import { LimitAutomationReadOnly } from './editor/LimitAutomationReadOnly';
import { LongTermGoalAutomationReadOnly } from './editor/LongTermGoalAutomationReadOnly';
import { PercentageAutomationReadOnly } from './editor/PercentageAutomationReadOnly';
import { RefillAutomationReadOnly } from './editor/RefillAutomationReadOnly';
import { RemainderAutomationReadOnly } from './editor/RemainderAutomationReadOnly';
import { ScheduleAutomationReadOnly } from './editor/ScheduleAutomationReadOnly';
type BudgetAutomationReadOnlyProps = {
state: ReducerState;
categoryNameMap: Record<string, string>;
isEditing: boolean;
setIsEditing: Dispatch<SetStateAction<boolean>>;
onDelete?: () => void;
style?: CSSProperties;
inline?: boolean;
};
export function BudgetAutomationReadOnly({
state,
categoryNameMap,
isEditing,
setIsEditing,
onDelete,
style,
inline,
}: BudgetAutomationReadOnlyProps) {
const { t } = useTranslation();
let automationReadOnly;
switch (state.displayType) {
case 'limit':
automationReadOnly = (
<LimitAutomationReadOnly template={state.template} />
);
break;
case 'refill':
automationReadOnly = <RefillAutomationReadOnly />;
break;
case 'fixed':
automationReadOnly = (
<FixedAutomationReadOnly template={state.template} />
);
break;
case 'schedule':
automationReadOnly = (
<ScheduleAutomationReadOnly template={state.template} />
);
break;
case 'percentage':
automationReadOnly = (
<PercentageAutomationReadOnly
template={state.template}
categoryNameMap={categoryNameMap}
/>
);
break;
case 'historical':
automationReadOnly = (
<HistoricalAutomationReadOnly template={state.template} />
);
break;
case 'by':
automationReadOnly = (
<BySaveAutomationReadOnly template={state.template} />
);
break;
case 'remainder':
automationReadOnly = (
<RemainderAutomationReadOnly template={state.template} />
);
break;
case 'goal':
automationReadOnly = (
<LongTermGoalAutomationReadOnly template={state.template} />
);
break;
default:
state satisfies never;
automationReadOnly = (
<Text>
<Trans>Unrecognized automation type.</Trans>
</Text>
);
break;
}
return (
<SpaceBetween gap={10} style={{ alignItems: 'center', ...style }}>
{inline && (
<View
style={{
borderLeft: `1px solid ${theme.tableBorder}`,
height: 'calc(100% - 4px)',
}}
/>
)}
<Text style={{ color: theme.tableText, fontSize: 13 }}>
{automationReadOnly}
</Text>
<View style={{ flex: 1 }} />
<Button
variant="bare"
onPress={() => setIsEditing(prev => !prev)}
style={{ padding: 2 }}
aria-label={t('Edit template')}
>
{isEditing ? (
<SvgCheveronUp style={{ width: 16, height: 16, color: 'inherit' }} />
) : (
<SvgCheveronDown
style={{ width: 16, height: 16, color: 'inherit' }}
/>
)}
</Button>
<Button
variant="bare"
onPress={onDelete}
style={{ padding: 5 }}
aria-label={t('Delete template')}
>
<SvgDelete style={{ width: 8, height: 8, color: 'inherit' }} />
</Button>
</SpaceBetween>
);
}

View File

@@ -116,10 +116,6 @@ export function selectAscDesc(
: defaultAscDesc;
}
export function getDisplayValue<T>(obj: T | null | undefined, name: keyof T) {
return obj ? obj[name] : '';
}
// Decides whether a rule result should be applied to a field while the user is
// entering a new transaction. By default rules only fill fields the user left
// empty, so their manual input isn't overwritten. The exception is the notes

View File

@@ -1,35 +0,0 @@
import * as monthUtils from '@actual-app/core/shared/months';
function isMonthOnlyDate(s: string) {
return s.includes('-') && s.split('-').length === 2;
}
function toMonth(dateOrMonth: string) {
return isMonthOnlyDate(dateOrMonth)
? dateOrMonth
: monthUtils.monthFromDate(dateOrMonth);
}
/**
* Given a stored sliding-window start/end (which represent a window length),
* return the live range anchored to the current day.
*
* Example: start=2024-01, end=2024-03 => 3-month window => liveStart is currentMonth-2.
*/
export function getLiveSlidingWindowRange(
start: string,
end: string,
today: string = monthUtils.currentDay(),
) {
const startMonth = toMonth(start);
const endMonth = toMonth(end);
const offset = monthUtils.differenceInCalendarMonths(endMonth, startMonth);
const liveEndMonth = monthUtils.monthFromDate(today);
const liveStartMonth = monthUtils.subMonths(liveEndMonth, offset);
return {
startDate: monthUtils.firstDayOfMonth(liveStartMonth),
endDate: today,
};
}

View File

@@ -1,20 +0,0 @@
import { useEffect, useRef } from 'react';
import type { DependencyList, EffectCallback } from 'react';
/**
* A version of useEffect that doesn't run on the initial mount.
*/
export function useEffectAfterMount(
effect: EffectCallback,
deps?: DependencyList | undefined,
) {
const isFirstRender = useRef(true);
useEffect(() => {
if (!isFirstRender.current) {
return effect();
}
isFirstRender.current = false;
// oxlint-disable-next-line react-hooks/exhaustive-deps -- caller owns deps
}, deps);
}

View File

@@ -1,159 +0,0 @@
import { useEffect, useState } from 'react';
import { send } from '@actual-app/core/platform/client/connection';
import { setCachedUserPreferences } from '@actual-app/core/shared/formulas/customFunctions';
import { HyperFormula } from 'hyperformula';
import { bootstrapHyperFormula } from '#util/bootstrapHyperFormula';
import { useGlobalPref } from './useGlobalPref';
import { useLocale } from './useLocale';
bootstrapHyperFormula();
type TransactionContext = {
amount?: number;
date?: string;
notes?: string;
imported_payee?: string;
account?: string;
category?: string;
payee?: string;
cleared?: boolean;
reconciled?: boolean;
[key: string]: string | number | boolean | undefined;
};
export function useTransactionFormulaExecution(
formula: string,
transaction: TransactionContext,
) {
const locale = useLocale();
const [language] = useGlobalPref('language');
const [result, setResult] = useState<number | string | boolean | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
async function executeFormula() {
let hfInstance: ReturnType<typeof HyperFormula.buildEmpty> | null = null;
if (!formula || !formula.startsWith('=')) {
setResult(null);
setError('Formula must start with =');
return;
}
try {
const browserLocale =
typeof navigator === 'undefined' ? undefined : navigator.language;
const formulaLocale = language || browserLocale || 'en-US';
try {
setCachedUserPreferences(
await send('formula-load-user-preferences', {
selectedLocale: language,
browserLocale,
}),
);
} catch (err) {
console.error('Error loading formula preferences:', err);
}
// Create HyperFormula instance
hfInstance = HyperFormula.buildEmpty({
licenseKey: 'gpl-v3',
language: 'enUS',
localeLang: formulaLocale,
dateFormats: ['DD/MM/YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD'],
context: {
// No server prefetch in preview
balanceOfPrefetch: new Map(),
},
});
// Add a sheet
const sheetName = hfInstance.addSheet('Sheet1');
const sheetId = hfInstance.getSheetId(sheetName);
if (sheetId === undefined) {
throw new Error('Failed to create sheet');
}
// Add named ranges for each transaction field
let row = 1;
const fieldValues: Record<string, number | string | boolean> = {
today: new Date().toISOString().split('T')[0],
...transaction,
};
for (const [key, value] of Object.entries(fieldValues)) {
if (
value !== undefined &&
value !== null &&
typeof value !== 'object' &&
!key.startsWith('_')
) {
// Set the value in a cell
hfInstance.setCellContents({ sheet: sheetId, col: 0, row }, [
[value],
]);
// Create a named range for this field
hfInstance.addNamedExpression(
key,
`=Sheet1!$A$${row + 1}`, // +1 because HyperFormula uses 0-based rows but formulas use 1-based
);
row++;
}
}
// Set the formula in row 0
hfInstance.setCellContents({ sheet: sheetId, col: 0, row: 0 }, [
[formula],
]);
// Get the result
const cellValue = hfInstance.getCellValue({
sheet: sheetId,
col: 0,
row: 0,
});
if (cancelled) return;
// Check if there's an error
if (cellValue && typeof cellValue === 'object' && 'type' in cellValue) {
setError(`Formula error: ${cellValue.type}`);
setResult(null);
} else {
setResult(cellValue as number | string | boolean);
setError(null);
}
} catch (err) {
if (cancelled) return;
console.error('Formula execution error:', err);
setError(err instanceof Error ? err.message : 'Unknown error');
setResult(null);
} finally {
try {
hfInstance?.destroy();
} catch (err) {
console.error('Error destroying HyperFormula instance:', err);
setError('Error destroying HyperFormula instance');
setResult(null);
}
}
}
void executeFormula();
return () => {
cancelled = true;
};
}, [formula, transaction, locale, language]);
return { result, error };
}

View File

@@ -26,15 +26,6 @@ function stripTrailingPunctuation(url: string): string {
return url.replace(TRAILING_PUNCTUATION_REGEX, '');
}
/**
* Checks if a URL is a file path
*/
export function isFilePathUrl(url: string): boolean {
return (
url.startsWith('/') || /^[A-Z]:\\/i.test(url) || url.startsWith('file://')
);
}
/**
* Normalizes a URL by adding protocol if missing
*/

View File

@@ -89,7 +89,6 @@ export function configureAppStore({
export type AppStore = ReturnType<typeof configureAppStore>;
export type RootState = ReturnType<AppStore['getState']>;
export type AppDispatch = AppStore['dispatch'];
export type GetRootState = AppStore['getState'];
export type ExtraArguments = {
queryClient: QueryClient;
};

View File

@@ -21,7 +21,6 @@
"devDependencies": {
"@actual-app/core": "workspace:*",
"@actual-app/web": "workspace:*",
"@electron/notarize": "3.1.1",
"@electron/rebuild": "^4.0.3",
"@playwright/test": "1.59.1",
"@types/copyfiles": "^2",

View File

@@ -25,11 +25,13 @@
"@r74tech/docusaurus-plugin-panzoom": "^2.4.2",
"prism-react-renderer": "^2.4.1",
"react": "19.2.7",
"react-dom": "19.2.7"
"react-dom": "19.2.7",
"unist-util-visit-parents": "^6.0.0"
},
"devDependencies": {
"@actual-app/ci-actions": "workspace:*",
"@docusaurus/module-type-aliases": "^3.10.1",
"@docusaurus/types": "^3.10.1",
"@types/react": "^19.2.17"
},
"browserslist": {

View File

@@ -9,6 +9,7 @@
},
"devDependencies": {
"@oxlint/plugins": "^1.69.0",
"@types/estree": "^1.0.8",
"eslint-vitest-rule-tester": "^3.1.0",
"vitest": "^4.1.8"
}

View File

@@ -1,101 +0,0 @@
// @ts-strict-ignore
export function makeSpreadsheet() {
const cells = {};
return {
observers: [],
_getNode(sheetName, name) {
const resolvedName = `${sheetName}!${name}`;
const existing = cells[resolvedName];
if (existing) {
return existing;
}
cells[resolvedName] = {
name: resolvedName,
sheet: sheetName,
value: null,
};
return cells[resolvedName];
},
prewarmCache(sheetName = '__global', name, value) {
this._getNode(sheetName, name).value = value;
},
bind(sheetName, binding, cb) {
const { name } = binding;
const resolvedName = `${sheetName}!${name}`;
if (!this.observers[resolvedName]) {
this.observers[resolvedName] = [];
}
this.observers[resolvedName].push(cb);
const node = this._getNode(sheetName, name);
cb(node);
// bind returns a function which unsubscribes itself. In this mock
// it's a noop.
return () => {
this.observers[resolvedName] = this.observers[resolvedName].filter(
x => x !== cb,
);
};
},
create(sheetName, name, expr) {
this.set(sheetName, name, expr);
},
get(sheetName, name) {
return this._getNode(sheetName, name);
},
getValue(sheetName, name) {
return this._getNode(sheetName, name).value;
},
set(sheetName, name, expr) {
const node = this._getNode(sheetName, name);
node.value = expr;
const resolvedName = `${sheetName}!${name}`;
if (this.observers[resolvedName]) {
this.observers[resolvedName].forEach(cb => cb(node));
}
},
getCellNames(sheetName) {
const names = Object.keys(cells);
if (sheetName) {
return names.filter(name => name.startsWith(sheetName + '!'));
}
return names;
},
getCells() {
return cells;
},
setCells(cells) {
Object.keys(cells).forEach(sheet => {
Object.keys(cells[sheet]).forEach(name => {
this.set(sheet, name, cells[sheet][name]);
});
});
},
deleteCells(cells) {
Object.keys(cells).forEach(sheet => {
cells[sheet].forEach(name => {
this.set(sheet, name, '');
});
});
},
batchChange(batch) {
this.setCells(batch.updateCells);
this.deleteCells(batch.deleteCells);
},
};
}

View File

@@ -114,11 +114,11 @@
"@types/convict": "^6",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/express-actuator": "^1.8.3",
"@types/jws": "^3.2.11",
"@types/node": "^22.19.21",
"@types/supertest": "^7.2.0",
"@typescript/native-preview": "beta",
"cross-env": "^10.1.0",
"http-proxy-middleware": "^4.1.1",
"nodemon": "^3.1.14",
"supertest": "^7.2.2",

View File

@@ -72,13 +72,6 @@ export const mockTransactions: { transactions: Transactions } = {
},
};
export const mockUnknownError = {
summary: "Couldn't update account balances",
detail: 'Request to Institution returned an error',
type: 'UnknownRequestError',
status_code: 500,
};
export const mockAccountDetails: { account: GoCardlessAccountDetails } = {
account: {
resourceId: 'PL00000000000000000987654321',

View File

@@ -1,4 +1,4 @@
import { getAccountDb, isAdmin } from '#account-db';
import { isAdmin } from '#account-db';
import { FileNotFound, GenericFileError } from '#app-sync/errors';
import type { WrappedDatabase } from '#db';
import { isValidFileId, isValidGroupId } from '#util/paths';
@@ -300,6 +300,4 @@ class FilesService {
}
}
const filesService = new FilesService(getAccountDb());
export { filesService, FilesService, File, FileUpdate };
export { FilesService, File, FileUpdate };

View File

@@ -1,44 +0,0 @@
import type { ServerOptions } from 'https';
type LoginMethod = 'password' | 'header' | 'openid';
export type Config = {
mode: 'test' | 'development';
loginMethod?: LoginMethod;
allowedLoginMethods: LoginMethod[];
trustedProxies: string[];
trustedAuthProxies?: string[];
dataDir: string;
projectRoot: string;
port: number;
hostname: string;
serverFiles: string;
userFiles: string;
webRoot: string;
https?: {
key: string;
cert: string;
} & ServerOptions;
upload?: {
fileSizeSyncLimitMB: number;
syncEncryptedFileSizeLimitMB: number;
fileSizeLimitMB: number;
};
openId?: {
issuer:
| string
| {
name: string;
authorization_endpoint: string;
token_endpoint: string;
userinfo_endpoint: string;
};
client_id: string;
client_secret: string;
server_hostname: string;
authMethod?: 'openid' | 'oauth2';
};
token_expiration?: 'never' | 'openid-provider' | number;
enforceOpenId: boolean;
userCreationMode?: 'manual' | 'login';
};

View File

@@ -51,14 +51,6 @@ export function getOwnerId() {
return id;
}
export function getFileOwnerId(fileId) {
const { owner } =
getAccountDb().first(`SELECT files.owner FROM files WHERE files.id = ?`, [
fileId,
]) || {};
return owner;
}
export function getAllUsers() {
return getAccountDb().all(
`SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(owner,0) as owner, role
@@ -74,20 +66,6 @@ export function insertUser(userId, userName, displayName, enabled, role) {
);
}
export function updateUser(userId, userName, displayName, enabled) {
if (!userId || !userName) {
throw new Error('Invalid user parameters');
}
try {
getAccountDb().mutate(
'UPDATE users SET user_name = ?, display_name = ?, enabled = ? WHERE id = ?',
[userName, displayName, enabled, userId],
);
} catch (error) {
throw new Error(`Failed to update user: ${error.message}`);
}
}
export function updateUserWithRole(
userId,
userName,

View File

@@ -1,22 +1,4 @@
import { createInterface, cursorTo } from 'node:readline';
export async function prompt(message: string) {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
const promise = new Promise(resolve => {
rl.question(message, answer => {
resolve(answer);
rl.close();
});
});
const answer = await promise;
return answer;
}
import { cursorTo } from 'node:readline';
export async function promptPassword() {
const password = await askForPassword('Enter a password, then press enter: ');

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Add the knip tool to automatically find unused files, dependencies and exports, and remove the dead code it surfaced.

564
yarn.lock
View File

@@ -77,6 +77,8 @@ __metadata:
"@storybook/react-vite": "npm:^10.4.4"
"@svgr/babel-plugin-add-jsx-attribute": "npm:^8.0.0"
"@svgr/cli": "npm:^8.1.0"
"@svgr/core": "npm:^8.1.0"
"@testing-library/react": "npm:16.3.2"
"@types/react": "npm:^19.2.17"
"@typescript/native-preview": "npm:beta"
"@vitejs/plugin-react": "npm:^6.0.2"
@@ -171,7 +173,6 @@ __metadata:
"@types/convict": "npm:^6"
"@types/cors": "npm:^2.8.19"
"@types/express": "npm:^5.0.6"
"@types/express-actuator": "npm:^1.8.3"
"@types/jws": "npm:^3.2.11"
"@types/node": "npm:^22.19.21"
"@types/supertest": "npm:^7.2.0"
@@ -181,6 +182,7 @@ __metadata:
better-sqlite3: "npm:^12.10.0"
convict: "npm:^6.2.5"
cors: "npm:^2.8.6"
cross-env: "npm:^10.1.0"
date-fns: "npm:^4.4.0"
debug: "npm:^4.4.3"
express: "npm:^5.2.1"
@@ -3307,7 +3309,7 @@ __metadata:
languageName: node
linkType: hard
"@docusaurus/types@npm:3.10.1":
"@docusaurus/types@npm:3.10.1, @docusaurus/types@npm:^3.10.1":
version: 3.10.1
resolution: "@docusaurus/types@npm:3.10.1"
dependencies:
@@ -3501,16 +3503,6 @@ __metadata:
languageName: node
linkType: hard
"@electron/notarize@npm:3.1.1":
version: 3.1.1
resolution: "@electron/notarize@npm:3.1.1"
dependencies:
debug: "npm:^4.4.0"
promise-retry: "npm:^2.0.1"
checksum: 10/7539f5db1d12758c56edabcbad4c3b748505cf72bdb2770c70ab88e52481cc3b737df6bd0edb60122debebd30a63c6c686cc51c19802ae61762f6c438e88dece
languageName: node
linkType: hard
"@electron/osx-sign@npm:1.3.3":
version: 1.3.3
resolution: "@electron/osx-sign@npm:1.3.3"
@@ -5037,6 +5029,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-android-arm-eabi@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-android-arm-eabi@npm:0.135.0"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
"@oxc-parser/binding-android-arm64@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-android-arm64@npm:0.127.0"
@@ -5044,6 +5043,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-android-arm64@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-android-arm64@npm:0.135.0"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@oxc-parser/binding-darwin-arm64@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-darwin-arm64@npm:0.127.0"
@@ -5051,6 +5057,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-darwin-arm64@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-darwin-arm64@npm:0.135.0"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@oxc-parser/binding-darwin-x64@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-darwin-x64@npm:0.127.0"
@@ -5058,6 +5071,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-darwin-x64@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-darwin-x64@npm:0.135.0"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@oxc-parser/binding-freebsd-x64@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-freebsd-x64@npm:0.127.0"
@@ -5065,6 +5085,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-freebsd-x64@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-freebsd-x64@npm:0.135.0"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@oxc-parser/binding-linux-arm-gnueabihf@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.127.0"
@@ -5072,6 +5099,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-linux-arm-gnueabihf@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.135.0"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@oxc-parser/binding-linux-arm-musleabihf@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-linux-arm-musleabihf@npm:0.127.0"
@@ -5079,6 +5113,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-linux-arm-musleabihf@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-linux-arm-musleabihf@npm:0.135.0"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@oxc-parser/binding-linux-arm64-gnu@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-linux-arm64-gnu@npm:0.127.0"
@@ -5086,6 +5127,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-linux-arm64-gnu@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-linux-arm64-gnu@npm:0.135.0"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@oxc-parser/binding-linux-arm64-musl@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-linux-arm64-musl@npm:0.127.0"
@@ -5093,6 +5141,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-linux-arm64-musl@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-linux-arm64-musl@npm:0.135.0"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@oxc-parser/binding-linux-ppc64-gnu@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-linux-ppc64-gnu@npm:0.127.0"
@@ -5100,6 +5155,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-linux-ppc64-gnu@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-linux-ppc64-gnu@npm:0.135.0"
conditions: os=linux & cpu=ppc64 & libc=glibc
languageName: node
linkType: hard
"@oxc-parser/binding-linux-riscv64-gnu@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-linux-riscv64-gnu@npm:0.127.0"
@@ -5107,6 +5169,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-linux-riscv64-gnu@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-linux-riscv64-gnu@npm:0.135.0"
conditions: os=linux & cpu=riscv64 & libc=glibc
languageName: node
linkType: hard
"@oxc-parser/binding-linux-riscv64-musl@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-linux-riscv64-musl@npm:0.127.0"
@@ -5114,6 +5183,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-linux-riscv64-musl@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-linux-riscv64-musl@npm:0.135.0"
conditions: os=linux & cpu=riscv64 & libc=musl
languageName: node
linkType: hard
"@oxc-parser/binding-linux-s390x-gnu@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-linux-s390x-gnu@npm:0.127.0"
@@ -5121,6 +5197,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-linux-s390x-gnu@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-linux-s390x-gnu@npm:0.135.0"
conditions: os=linux & cpu=s390x & libc=glibc
languageName: node
linkType: hard
"@oxc-parser/binding-linux-x64-gnu@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-linux-x64-gnu@npm:0.127.0"
@@ -5128,6 +5211,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-linux-x64-gnu@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-linux-x64-gnu@npm:0.135.0"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@oxc-parser/binding-linux-x64-musl@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-linux-x64-musl@npm:0.127.0"
@@ -5135,6 +5225,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-linux-x64-musl@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-linux-x64-musl@npm:0.135.0"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@oxc-parser/binding-openharmony-arm64@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-openharmony-arm64@npm:0.127.0"
@@ -5142,6 +5239,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-openharmony-arm64@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-openharmony-arm64@npm:0.135.0"
conditions: os=openharmony & cpu=arm64
languageName: node
linkType: hard
"@oxc-parser/binding-wasm32-wasi@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-wasm32-wasi@npm:0.127.0"
@@ -5153,6 +5257,17 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-wasm32-wasi@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-wasm32-wasi@npm:0.135.0"
dependencies:
"@emnapi/core": "npm:1.10.0"
"@emnapi/runtime": "npm:1.10.0"
"@napi-rs/wasm-runtime": "npm:^1.1.4"
conditions: cpu=wasm32
languageName: node
linkType: hard
"@oxc-parser/binding-win32-arm64-msvc@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-win32-arm64-msvc@npm:0.127.0"
@@ -5160,6 +5275,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-win32-arm64-msvc@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-win32-arm64-msvc@npm:0.135.0"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@oxc-parser/binding-win32-ia32-msvc@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-win32-ia32-msvc@npm:0.127.0"
@@ -5167,6 +5289,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-win32-ia32-msvc@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-win32-ia32-msvc@npm:0.135.0"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@oxc-parser/binding-win32-x64-msvc@npm:0.127.0":
version: 0.127.0
resolution: "@oxc-parser/binding-win32-x64-msvc@npm:0.127.0"
@@ -5174,6 +5303,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-parser/binding-win32-x64-msvc@npm:0.135.0":
version: 0.135.0
resolution: "@oxc-parser/binding-win32-x64-msvc@npm:0.135.0"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@oxc-project/types@npm:=0.133.0":
version: 0.133.0
resolution: "@oxc-project/types@npm:0.133.0"
@@ -5181,7 +5317,7 @@ __metadata:
languageName: node
linkType: hard
"@oxc-project/types@npm:=0.135.0":
"@oxc-project/types@npm:=0.135.0, @oxc-project/types@npm:^0.135.0":
version: 0.135.0
resolution: "@oxc-project/types@npm:0.135.0"
checksum: 10/b56f811e0e2f286d447cceab471dc199c99e1519fbc0afd50ff04c91a83eb7fb12efbd55238555a4e60aeed451e951f42438c63925037d132fddadbedca00ef3
@@ -5202,6 +5338,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-android-arm-eabi@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-android-arm-eabi@npm:11.21.3"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
"@oxc-resolver/binding-android-arm64@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-android-arm64@npm:11.20.0"
@@ -5209,6 +5352,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-android-arm64@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-android-arm64@npm:11.21.3"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@oxc-resolver/binding-darwin-arm64@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.20.0"
@@ -5216,6 +5366,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-darwin-arm64@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.21.3"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@oxc-resolver/binding-darwin-x64@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-darwin-x64@npm:11.20.0"
@@ -5223,6 +5380,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-darwin-x64@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-darwin-x64@npm:11.21.3"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@oxc-resolver/binding-freebsd-x64@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-freebsd-x64@npm:11.20.0"
@@ -5230,6 +5394,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-freebsd-x64@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-freebsd-x64@npm:11.21.3"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.20.0"
@@ -5237,6 +5408,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.21.3"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-arm-musleabihf@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-linux-arm-musleabihf@npm:11.20.0"
@@ -5244,6 +5422,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-arm-musleabihf@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-linux-arm-musleabihf@npm:11.21.3"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-arm64-gnu@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-linux-arm64-gnu@npm:11.20.0"
@@ -5251,6 +5436,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-arm64-gnu@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-linux-arm64-gnu@npm:11.21.3"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-arm64-musl@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-linux-arm64-musl@npm:11.20.0"
@@ -5258,6 +5450,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-arm64-musl@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-linux-arm64-musl@npm:11.21.3"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-ppc64-gnu@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-linux-ppc64-gnu@npm:11.20.0"
@@ -5265,6 +5464,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-ppc64-gnu@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-linux-ppc64-gnu@npm:11.21.3"
conditions: os=linux & cpu=ppc64 & libc=glibc
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-riscv64-gnu@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-linux-riscv64-gnu@npm:11.20.0"
@@ -5272,6 +5478,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-riscv64-gnu@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-linux-riscv64-gnu@npm:11.21.3"
conditions: os=linux & cpu=riscv64 & libc=glibc
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-riscv64-musl@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-linux-riscv64-musl@npm:11.20.0"
@@ -5279,6 +5492,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-riscv64-musl@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-linux-riscv64-musl@npm:11.21.3"
conditions: os=linux & cpu=riscv64 & libc=musl
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-s390x-gnu@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-linux-s390x-gnu@npm:11.20.0"
@@ -5286,6 +5506,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-s390x-gnu@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-linux-s390x-gnu@npm:11.21.3"
conditions: os=linux & cpu=s390x & libc=glibc
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-x64-gnu@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-linux-x64-gnu@npm:11.20.0"
@@ -5293,6 +5520,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-x64-gnu@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-linux-x64-gnu@npm:11.21.3"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-x64-musl@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-linux-x64-musl@npm:11.20.0"
@@ -5300,6 +5534,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-linux-x64-musl@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-linux-x64-musl@npm:11.21.3"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@oxc-resolver/binding-openharmony-arm64@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-openharmony-arm64@npm:11.20.0"
@@ -5307,6 +5548,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-openharmony-arm64@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-openharmony-arm64@npm:11.21.3"
conditions: os=openharmony & cpu=arm64
languageName: node
linkType: hard
"@oxc-resolver/binding-wasm32-wasi@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-wasm32-wasi@npm:11.20.0"
@@ -5318,6 +5566,17 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-wasm32-wasi@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-wasm32-wasi@npm:11.21.3"
dependencies:
"@emnapi/core": "npm:1.11.0"
"@emnapi/runtime": "npm:1.11.0"
"@napi-rs/wasm-runtime": "npm:^1.1.5"
conditions: cpu=wasm32
languageName: node
linkType: hard
"@oxc-resolver/binding-win32-arm64-msvc@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-win32-arm64-msvc@npm:11.20.0"
@@ -5325,6 +5584,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-win32-arm64-msvc@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-win32-arm64-msvc@npm:11.21.3"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@oxc-resolver/binding-win32-x64-msvc@npm:11.20.0":
version: 11.20.0
resolution: "@oxc-resolver/binding-win32-x64-msvc@npm:11.20.0"
@@ -5332,6 +5598,13 @@ __metadata:
languageName: node
linkType: hard
"@oxc-resolver/binding-win32-x64-msvc@npm:11.21.3":
version: 11.21.3
resolution: "@oxc-resolver/binding-win32-x64-msvc@npm:11.21.3"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@oxfmt/binding-android-arm-eabi@npm:0.44.0":
version: 0.44.0
resolution: "@oxfmt/binding-android-arm-eabi@npm:0.44.0"
@@ -7399,7 +7672,7 @@ __metadata:
languageName: node
linkType: hard
"@svgr/core@npm:8.1.0":
"@svgr/core@npm:8.1.0, @svgr/core@npm:^8.1.0":
version: 8.1.0
resolution: "@svgr/core@npm:8.1.0"
dependencies:
@@ -8093,15 +8366,6 @@ __metadata:
languageName: node
linkType: hard
"@types/express-actuator@npm:^1.8.3":
version: 1.8.3
resolution: "@types/express-actuator@npm:1.8.3"
dependencies:
"@types/express": "npm:*"
checksum: 10/0da489ce311c36dcf6ee1a03ec082742b181aa8ac95eb884fe09f5635ee5621b575100133b3589e7e8f8056da608010bd7232c0c21aeee484735f5b8fce73948
languageName: node
linkType: hard
"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^5.0.0":
version: 5.1.0
resolution: "@types/express-serve-static-core@npm:5.1.0"
@@ -9372,6 +9636,7 @@ __metadata:
eslint-plugin-typescript-paths: "npm:^0.0.33"
glob-hasher-linux-arm64-gnu: "npm:1.4.2"
husky: "npm:^9.1.7"
knip: "npm:^6.17.1"
lage: "npm:^2.15.13"
minimatch: "npm:^10.2.5"
nano-staged: "npm:^1.0.2"
@@ -12951,7 +13216,6 @@ __metadata:
"@actual-app/core": "workspace:*"
"@actual-app/sync-server": "workspace:*"
"@actual-app/web": "workspace:*"
"@electron/notarize": "npm:3.1.1"
"@electron/rebuild": "npm:^4.0.3"
"@playwright/test": "npm:1.59.1"
"@types/copyfiles": "npm:^2"
@@ -13131,6 +13395,7 @@ __metadata:
"@docusaurus/preset-classic": "npm:^3.10.1"
"@docusaurus/theme-common": "npm:^3.10.1"
"@docusaurus/theme-mermaid": "npm:^3.10.1"
"@docusaurus/types": "npm:^3.10.1"
"@easyops-cn/docusaurus-search-local": "npm:^0.55.2"
"@mdx-js/react": "npm:^3.1.1"
"@r74tech/docusaurus-plugin-panzoom": "npm:^2.4.2"
@@ -13138,6 +13403,7 @@ __metadata:
prism-react-renderer: "npm:^2.4.1"
react: "npm:19.2.7"
react-dom: "npm:19.2.7"
unist-util-visit-parents: "npm:^6.0.0"
languageName: unknown
linkType: soft
@@ -13998,6 +14264,7 @@ __metadata:
resolution: "eslint-plugin-actual@workspace:packages/eslint-plugin-actual"
dependencies:
"@oxlint/plugins": "npm:^1.69.0"
"@types/estree": "npm:^1.0.8"
eslint-vitest-rule-tester: "npm:^3.1.0"
vitest: "npm:^4.1.8"
languageName: unknown
@@ -14659,6 +14926,15 @@ __metadata:
languageName: node
linkType: hard
"fd-package-json@npm:^2.0.0":
version: 2.0.0
resolution: "fd-package-json@npm:2.0.0"
dependencies:
walk-up-path: "npm:^4.0.0"
checksum: 10/e595a1a23f8e208815cdcf26c92218240da00acce80468324408dc4a5cb6c26b6efb5076f0458a02f044562a1e60253731187a627d5416b4961468ddfc0ae426
languageName: node
linkType: hard
"fd-slicer@npm:~1.1.0":
version: 1.1.0
resolution: "fd-slicer@npm:1.1.0"
@@ -14907,6 +15183,17 @@ __metadata:
languageName: node
linkType: hard
"formatly@npm:^0.3.0":
version: 0.3.0
resolution: "formatly@npm:0.3.0"
dependencies:
fd-package-json: "npm:^2.0.0"
bin:
formatly: bin/index.mjs
checksum: 10/0e5a9cbb826d93171b00c283e20e6a564a16e7bc3839e695790347a1f23e3536a88d613f5cabd07403d60b7bdffe179987c88b1fc2900a9be49eea01ffbe4244
languageName: node
linkType: hard
"formidable@npm:^3.5.4":
version: 3.5.4
resolution: "formidable@npm:3.5.4"
@@ -15223,6 +15510,15 @@ __metadata:
languageName: node
linkType: hard
"get-tsconfig@npm:4.14.0":
version: 4.14.0
resolution: "get-tsconfig@npm:4.14.0"
dependencies:
resolve-pkg-maps: "npm:^1.0.0"
checksum: 10/f5626971905ca386c9ddeb302504e8a2301b9c05641803267223ebd50b7c81aaf9324d29cf9f4e4ff3f245632c3392aa83719dc6cb3e98b4e4a1702a27c5cc5d
languageName: node
linkType: hard
"github-from-package@npm:0.0.0":
version: 0.0.0
resolution: "github-from-package@npm:0.0.0"
@@ -17323,6 +17619,15 @@ __metadata:
languageName: node
linkType: hard
"jiti@npm:^2.7.0":
version: 2.7.0
resolution: "jiti@npm:2.7.0"
bin:
jiti: lib/jiti-cli.mjs
checksum: 10/6d75a8dbd61dbee031aa0937fabb748ff8ddf370b971958cc704f5cf26b4c5bdc9dcd0563059b2627a2bd41d946fa0bc64f912fdc8981ca7945a9d63c74ad0f9
languageName: node
linkType: hard
"joi@npm:^17.9.2":
version: 17.13.4
resolution: "joi@npm:17.13.4"
@@ -17621,6 +17926,30 @@ __metadata:
languageName: node
linkType: hard
"knip@npm:^6.17.1":
version: 6.17.1
resolution: "knip@npm:6.17.1"
dependencies:
fdir: "npm:^6.5.0"
formatly: "npm:^0.3.0"
get-tsconfig: "npm:4.14.0"
jiti: "npm:^2.7.0"
oxc-parser: "npm:^0.135.0"
oxc-resolver: "npm:^11.20.0"
picomatch: "npm:^4.0.4"
smol-toml: "npm:^1.6.1"
strip-json-comments: "npm:5.0.3"
tinyglobby: "npm:^0.2.17"
unbash: "npm:^4.0.1"
yaml: "npm:^2.9.0"
zod: "npm:^4.1.11"
bin:
knip: bin/knip.js
knip-bun: bin/knip-bun.js
checksum: 10/d08a7ec333aa21367ac0f9b6f92081d95ff60d80b55c85fffea76d07da960036bb3ac7038afe68fec6dae938d51b1efef695d147201c917a90f36db0dbec1682
languageName: node
linkType: hard
"kuler@npm:^2.0.0":
version: 2.0.0
resolution: "kuler@npm:2.0.0"
@@ -20228,6 +20557,76 @@ __metadata:
languageName: node
linkType: hard
"oxc-parser@npm:^0.135.0":
version: 0.135.0
resolution: "oxc-parser@npm:0.135.0"
dependencies:
"@oxc-parser/binding-android-arm-eabi": "npm:0.135.0"
"@oxc-parser/binding-android-arm64": "npm:0.135.0"
"@oxc-parser/binding-darwin-arm64": "npm:0.135.0"
"@oxc-parser/binding-darwin-x64": "npm:0.135.0"
"@oxc-parser/binding-freebsd-x64": "npm:0.135.0"
"@oxc-parser/binding-linux-arm-gnueabihf": "npm:0.135.0"
"@oxc-parser/binding-linux-arm-musleabihf": "npm:0.135.0"
"@oxc-parser/binding-linux-arm64-gnu": "npm:0.135.0"
"@oxc-parser/binding-linux-arm64-musl": "npm:0.135.0"
"@oxc-parser/binding-linux-ppc64-gnu": "npm:0.135.0"
"@oxc-parser/binding-linux-riscv64-gnu": "npm:0.135.0"
"@oxc-parser/binding-linux-riscv64-musl": "npm:0.135.0"
"@oxc-parser/binding-linux-s390x-gnu": "npm:0.135.0"
"@oxc-parser/binding-linux-x64-gnu": "npm:0.135.0"
"@oxc-parser/binding-linux-x64-musl": "npm:0.135.0"
"@oxc-parser/binding-openharmony-arm64": "npm:0.135.0"
"@oxc-parser/binding-wasm32-wasi": "npm:0.135.0"
"@oxc-parser/binding-win32-arm64-msvc": "npm:0.135.0"
"@oxc-parser/binding-win32-ia32-msvc": "npm:0.135.0"
"@oxc-parser/binding-win32-x64-msvc": "npm:0.135.0"
"@oxc-project/types": "npm:^0.135.0"
dependenciesMeta:
"@oxc-parser/binding-android-arm-eabi":
optional: true
"@oxc-parser/binding-android-arm64":
optional: true
"@oxc-parser/binding-darwin-arm64":
optional: true
"@oxc-parser/binding-darwin-x64":
optional: true
"@oxc-parser/binding-freebsd-x64":
optional: true
"@oxc-parser/binding-linux-arm-gnueabihf":
optional: true
"@oxc-parser/binding-linux-arm-musleabihf":
optional: true
"@oxc-parser/binding-linux-arm64-gnu":
optional: true
"@oxc-parser/binding-linux-arm64-musl":
optional: true
"@oxc-parser/binding-linux-ppc64-gnu":
optional: true
"@oxc-parser/binding-linux-riscv64-gnu":
optional: true
"@oxc-parser/binding-linux-riscv64-musl":
optional: true
"@oxc-parser/binding-linux-s390x-gnu":
optional: true
"@oxc-parser/binding-linux-x64-gnu":
optional: true
"@oxc-parser/binding-linux-x64-musl":
optional: true
"@oxc-parser/binding-openharmony-arm64":
optional: true
"@oxc-parser/binding-wasm32-wasi":
optional: true
"@oxc-parser/binding-win32-arm64-msvc":
optional: true
"@oxc-parser/binding-win32-ia32-msvc":
optional: true
"@oxc-parser/binding-win32-x64-msvc":
optional: true
checksum: 10/c2574f725a776316c397f7f6398e8a8f9f0b3d5f0f358d4f9a0dee128c4188a58fcbd159eac9ff754a13fc29011a7cae90d7c5d07d10288410a3044a1d73cb47
languageName: node
linkType: hard
"oxc-resolver@npm:^11.19.1":
version: 11.20.0
resolution: "oxc-resolver@npm:11.20.0"
@@ -20294,6 +20693,72 @@ __metadata:
languageName: node
linkType: hard
"oxc-resolver@npm:^11.20.0":
version: 11.21.3
resolution: "oxc-resolver@npm:11.21.3"
dependencies:
"@oxc-resolver/binding-android-arm-eabi": "npm:11.21.3"
"@oxc-resolver/binding-android-arm64": "npm:11.21.3"
"@oxc-resolver/binding-darwin-arm64": "npm:11.21.3"
"@oxc-resolver/binding-darwin-x64": "npm:11.21.3"
"@oxc-resolver/binding-freebsd-x64": "npm:11.21.3"
"@oxc-resolver/binding-linux-arm-gnueabihf": "npm:11.21.3"
"@oxc-resolver/binding-linux-arm-musleabihf": "npm:11.21.3"
"@oxc-resolver/binding-linux-arm64-gnu": "npm:11.21.3"
"@oxc-resolver/binding-linux-arm64-musl": "npm:11.21.3"
"@oxc-resolver/binding-linux-ppc64-gnu": "npm:11.21.3"
"@oxc-resolver/binding-linux-riscv64-gnu": "npm:11.21.3"
"@oxc-resolver/binding-linux-riscv64-musl": "npm:11.21.3"
"@oxc-resolver/binding-linux-s390x-gnu": "npm:11.21.3"
"@oxc-resolver/binding-linux-x64-gnu": "npm:11.21.3"
"@oxc-resolver/binding-linux-x64-musl": "npm:11.21.3"
"@oxc-resolver/binding-openharmony-arm64": "npm:11.21.3"
"@oxc-resolver/binding-wasm32-wasi": "npm:11.21.3"
"@oxc-resolver/binding-win32-arm64-msvc": "npm:11.21.3"
"@oxc-resolver/binding-win32-x64-msvc": "npm:11.21.3"
dependenciesMeta:
"@oxc-resolver/binding-android-arm-eabi":
optional: true
"@oxc-resolver/binding-android-arm64":
optional: true
"@oxc-resolver/binding-darwin-arm64":
optional: true
"@oxc-resolver/binding-darwin-x64":
optional: true
"@oxc-resolver/binding-freebsd-x64":
optional: true
"@oxc-resolver/binding-linux-arm-gnueabihf":
optional: true
"@oxc-resolver/binding-linux-arm-musleabihf":
optional: true
"@oxc-resolver/binding-linux-arm64-gnu":
optional: true
"@oxc-resolver/binding-linux-arm64-musl":
optional: true
"@oxc-resolver/binding-linux-ppc64-gnu":
optional: true
"@oxc-resolver/binding-linux-riscv64-gnu":
optional: true
"@oxc-resolver/binding-linux-riscv64-musl":
optional: true
"@oxc-resolver/binding-linux-s390x-gnu":
optional: true
"@oxc-resolver/binding-linux-x64-gnu":
optional: true
"@oxc-resolver/binding-linux-x64-musl":
optional: true
"@oxc-resolver/binding-openharmony-arm64":
optional: true
"@oxc-resolver/binding-wasm32-wasi":
optional: true
"@oxc-resolver/binding-win32-arm64-msvc":
optional: true
"@oxc-resolver/binding-win32-x64-msvc":
optional: true
checksum: 10/e890dcca9bcb130c393493dffd763e9ed4b1bf8d32b5990cd094a7a5402bdb6dbd04e64893166e4531369552c37ae811a24206d663d6cf8fd65baf13437903ea
languageName: node
linkType: hard
"oxfmt@npm:^0.44.0":
version: 0.44.0
resolution: "oxfmt@npm:0.44.0"
@@ -23470,6 +23935,13 @@ __metadata:
languageName: node
linkType: hard
"resolve-pkg-maps@npm:^1.0.0":
version: 1.0.0
resolution: "resolve-pkg-maps@npm:1.0.0"
checksum: 10/0763150adf303040c304009231314d1e84c6e5ebfa2d82b7d94e96a6e82bacd1dcc0b58ae257315f3c8adb89a91d8d0f12928241cba2df1680fbe6f60bf99b0e
languageName: node
linkType: hard
"resolve@npm:^1.10.0, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.22.1, resolve@npm:^1.22.10, resolve@npm:^1.22.8":
version: 1.22.11
resolution: "resolve@npm:1.22.11"
@@ -24583,6 +25055,13 @@ __metadata:
languageName: node
linkType: hard
"smol-toml@npm:^1.6.1":
version: 1.6.1
resolution: "smol-toml@npm:1.6.1"
checksum: 10/9a0d86cc7f8abef429c915b373b9a1f369fe57a87efbbec46b967fb41dc28af753a2fa62c9c4848907c3b47c282be15c8854aa4e2942ef1fa86ff95a76d13856
languageName: node
linkType: hard
"snake-case@npm:^3.0.4":
version: 3.0.4
resolution: "snake-case@npm:3.0.4"
@@ -25149,6 +25628,13 @@ __metadata:
languageName: node
linkType: hard
"strip-json-comments@npm:5.0.3":
version: 5.0.3
resolution: "strip-json-comments@npm:5.0.3"
checksum: 10/3ccbf26f278220f785e4b71f8a719a6a063d72558cc63cb450924254af258a4f4c008b8c9b055373a680dc7bd525be9e543ad742c177f8a7667e0b726258e0e4
languageName: node
linkType: hard
"strip-json-comments@npm:^3.1.1":
version: 3.1.1
resolution: "strip-json-comments@npm:3.1.1"
@@ -26085,6 +26571,13 @@ __metadata:
languageName: node
linkType: hard
"unbash@npm:^4.0.1":
version: 4.0.1
resolution: "unbash@npm:4.0.1"
checksum: 10/1ad36bf3f1aeeb8ff72dfb6ddaa2262ee97576153bc40e8b62abfcad9a69445393b1916be5444402fa23b6759dd75d6ca52652a103a3be2dde98debff555d741
languageName: node
linkType: hard
"unbox-primitive@npm:^1.1.0":
version: 1.1.0
resolution: "unbox-primitive@npm:1.1.0"
@@ -26892,6 +27385,13 @@ __metadata:
languageName: node
linkType: hard
"walk-up-path@npm:^4.0.0":
version: 4.0.0
resolution: "walk-up-path@npm:4.0.0"
checksum: 10/6a230b20e5de296895116dc12b09dafaec1f72b8060c089533d296e241aff059dfaebe0d015c77467f857e4b40c78e08f7481add76f340233a1f34fa8af9ed63
languageName: node
linkType: hard
"watchpack@npm:^2.5.1":
version: 2.5.1
resolution: "watchpack@npm:2.5.1"
@@ -27746,6 +28246,15 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:^2.9.0":
version: 2.9.0
resolution: "yaml@npm:2.9.0"
bin:
yaml: bin.mjs
checksum: 10/9a95e8e08651c3d292ab6a5befeb5f57b76801caa097c75bb45c9a70ce19c1b11f57e87a6ef84a579ea070ed2c2c8ac541c88c0ae684d544d5f42c7e77d11b7b
languageName: node
linkType: hard
"yargs-parser@npm:^18.1.3":
version: 18.1.3
resolution: "yargs-parser@npm:18.1.3"
@@ -27845,6 +28354,13 @@ __metadata:
languageName: node
linkType: hard
"zod@npm:^4.1.11":
version: 4.4.3
resolution: "zod@npm:4.4.3"
checksum: 10/804b9a42aa8f35f2b3c5a8dff906291cb749115f83ee2afe3576d70b5b5c53c965365c7f4967690647a9c54af9838ff232a85ff9577a0a36c44b68bc6cdefe36
languageName: node
linkType: hard
"zwitch@npm:^2.0.0":
version: 2.0.4
resolution: "zwitch@npm:2.0.4"