fix(Map): Add common migration mechanism. ATTENTION! This is a non-reversible stored map settings commit — it means we do not guarantee that settings will work if you check out back. We’ve tried to migrate old settings, but it may not work well or may NOT work at all.

This commit is contained in:
DanSylvest
2025-09-24 11:03:17 +03:00
parent 34d3d92afd
commit 51ff4e7f36
26 changed files with 145 additions and 206 deletions

View File

@@ -1,4 +1,4 @@
import { MapUserSettings, SettingsWithVersion } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { MapUserSettings, SettingsWrapper } from '@/hooks/Mapper/mapRootProvider/types.ts';
export const REQUIRED_KEYS = [
'widgets',
@@ -19,11 +19,8 @@ export class MapUserSettingsParseError extends Error {
}
}
const isNumber = (v: unknown): v is number => typeof v === 'number' && !Number.isNaN(v);
/** Minimal check that an object matches SettingsWithVersion<*> */
const isSettingsWithVersion = (v: unknown): v is SettingsWithVersion<unknown> =>
typeof v === 'object' && v !== null && isNumber((v as any).version) && 'settings' in (v as any);
/** Minimal check that an object matches SettingsWrapper<*> */
const isSettings = (v: unknown): v is SettingsWrapper<unknown> => typeof v === 'object' && v !== null;
/** Ensure every required key is present */
const hasAllRequiredKeys = (v: unknown): v is Record<RequiredKeys, unknown> =>
@@ -52,8 +49,8 @@ export const parseMapUserSettings = (json: unknown): MapUserSettings => {
}
for (const key of REQUIRED_KEYS) {
if (!isSettingsWithVersion((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWithVersion<T>`);
if (!isSettings((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWrapper<T>`);
}
}

View File

@@ -5,6 +5,7 @@ import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { saveTextFile } from '@/hooks/Mapper/utils/saveToFile.ts';
import { SplitButton } from 'primereact/splitbutton';
import { loadTextFile } from '@/hooks/Mapper/utils';
import { applyMigrations } from '@/hooks/Mapper/mapRootProvider/migrations';
export const ImportExport = () => {
const {
@@ -22,8 +23,9 @@ export const ImportExport = () => {
}
try {
// INFO: WE NOT SUPPORT MIGRATIONS FOR OLD FILES AND Clipboard
const parsed = parseMapUserSettings(text);
if (applySettings(parsed)) {
if (applySettings(applyMigrations(parsed))) {
toast.current?.show({
severity: 'success',
summary: 'Import',
@@ -59,8 +61,9 @@ export const ImportExport = () => {
try {
const text = await loadTextFile();
// INFO: WE NOT SUPPORT MIGRATIONS FOR OLD FILES AND Clipboard
const parsed = parseMapUserSettings(text);
if (applySettings(parsed)) {
if (applySettings(applyMigrations(parsed))) {
toast.current?.show({
severity: 'success',
summary: 'Import',

View File

@@ -1,7 +1,6 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
@@ -9,6 +8,7 @@ import { callToastSuccess } from '@/hooks/Mapper/helpers';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { RemoteAdminSettingsResponse } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { applyMigrations } from '@/hooks/Mapper/mapRootProvider/migrations';
export const ServerSettings = () => {
const {
@@ -34,7 +34,8 @@ export const ServerSettings = () => {
}
try {
applySettings(parseMapUserSettings(res.default_settings));
//INFO: INSTEAD CHECK WE WILL TRY TO APPLY MIGRATION
applySettings(applyMigrations(JSON.parse(res.default_settings)));
callToastSuccess(toast.current, 'Settings synchronized successfully');
} catch (error) {
applySettings(createDefaultWidgetSettings());

View File

@@ -1,4 +1,4 @@
import { MapUserSettings, MigrationTypes, SettingsWithVersion } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { MapUserSettings, SettingsTypes, SettingsWrapper } from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
@@ -8,45 +8,44 @@ import {
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
import { SETTING_VERSIONS } from '@/hooks/Mapper/mapRootProvider/versions.ts';
import { STORED_SETTINGS_VERSION } from '@/hooks/Mapper/mapRootProvider/version.ts';
// TODO - we need provide and compare version
export const createWidgetSettingsWithVersion = <T>(version: number, settings: T) => {
return {
version,
settings,
};
export const createWidgetSettings = <T>(settings: T) => {
return settings;
};
export const createDefaultWidgetSettings = (): MapUserSettings => {
return {
killsWidget: createWidgetSettingsWithVersion(SETTING_VERSIONS.kills, DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createWidgetSettingsWithVersion(SETTING_VERSIONS.localWidget, DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createWidgetSettingsWithVersion(SETTING_VERSIONS.widgets, getDefaultWidgetProps()),
routes: createWidgetSettingsWithVersion(SETTING_VERSIONS.routes, DEFAULT_ROUTES_SETTINGS),
onTheMap: createWidgetSettingsWithVersion(SETTING_VERSIONS.onTheMap, DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createWidgetSettingsWithVersion(SETTING_VERSIONS.signatures, DEFAULT_SIGNATURE_SETTINGS),
interface: createWidgetSettingsWithVersion(SETTING_VERSIONS.interface, STORED_INTERFACE_DEFAULT_VALUES),
version: STORED_SETTINGS_VERSION,
migratedFromOld: true,
killsWidget: createWidgetSettings(DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createWidgetSettings(DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createWidgetSettings(getDefaultWidgetProps()),
routes: createWidgetSettings(DEFAULT_ROUTES_SETTINGS),
onTheMap: createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createWidgetSettings(DEFAULT_SIGNATURE_SETTINGS),
interface: createWidgetSettings(STORED_INTERFACE_DEFAULT_VALUES),
};
};
// INFO - in another case need to generate complex type - but looks like it unnecessary
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const getDefaultSettingsByType = (type: MigrationTypes): SettingsWithVersion<any> => {
export const getDefaultSettingsByType = (type: SettingsTypes): SettingsWrapper<any> => {
switch (type) {
case MigrationTypes.killsWidget:
return createWidgetSettingsWithVersion(SETTING_VERSIONS.kills, DEFAULT_KILLS_WIDGET_SETTINGS);
case MigrationTypes.localWidget:
return createWidgetSettingsWithVersion(SETTING_VERSIONS.localWidget, DEFAULT_WIDGET_LOCAL_SETTINGS);
case MigrationTypes.widgets:
return createWidgetSettingsWithVersion(SETTING_VERSIONS.widgets, getDefaultWidgetProps());
case MigrationTypes.routes:
return createWidgetSettingsWithVersion(SETTING_VERSIONS.routes, DEFAULT_ROUTES_SETTINGS);
case MigrationTypes.onTheMap:
return createWidgetSettingsWithVersion(SETTING_VERSIONS.onTheMap, DEFAULT_ON_THE_MAP_SETTINGS);
case MigrationTypes.signaturesWidget:
return createWidgetSettingsWithVersion(SETTING_VERSIONS.signatures, DEFAULT_SIGNATURE_SETTINGS);
case MigrationTypes.interface:
return createWidgetSettingsWithVersion(SETTING_VERSIONS.interface, STORED_INTERFACE_DEFAULT_VALUES);
case SettingsTypes.killsWidget:
return createWidgetSettings(DEFAULT_KILLS_WIDGET_SETTINGS);
case SettingsTypes.localWidget:
return createWidgetSettings(DEFAULT_WIDGET_LOCAL_SETTINGS);
case SettingsTypes.widgets:
return createWidgetSettings(getDefaultWidgetProps());
case SettingsTypes.routes:
return createWidgetSettings(DEFAULT_ROUTES_SETTINGS);
case SettingsTypes.onTheMap:
return createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS);
case SettingsTypes.signaturesWidget:
return createWidgetSettings(DEFAULT_SIGNATURE_SETTINGS);
case SettingsTypes.interface:
return createWidgetSettings(STORED_INTERFACE_DEFAULT_VALUES);
}
};

View File

@@ -6,7 +6,7 @@ import {
RemoteAdminSettingsResponse,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { applyMigrations } from '@/hooks/Mapper/mapRootProvider/migrations';
interface UseActualizeRemoteMapSettingsProps {
outCommand: OutCommandHandler;
@@ -42,7 +42,7 @@ export const useActualizeRemoteMapSettings = ({
}
try {
applySettings(parseMapUserSettings(res.default_settings));
applySettings(applyMigrations(JSON.parse(res.default_settings)));
} catch (error) {
applySettings(createDefaultWidgetSettings());
}

View File

@@ -7,7 +7,8 @@ import fastDeepEqual from 'fast-deep-equal';
import { OutCommandHandler } from '@/hooks/Mapper/types';
import { useActualizeRemoteMapSettings } from '@/hooks/Mapper/mapRootProvider/hooks/useActualizeRemoteMapSettings.ts';
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
import { applyMigrations, migrations } from '@/hooks/Mapper/mapRootProvider/migrations';
import { applyMigrations, extractData } from '@/hooks/Mapper/mapRootProvider/migrations';
import { LS_KEY, LS_KEY_LEGASY } from '@/hooks/Mapper/mapRootProvider/version.ts';
const EMPTY_OBJ = {};
@@ -15,7 +16,7 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
const [isReady, setIsReady] = useState(false);
const [hasOldSettings, setHasOldSettings] = useState(false);
const [mapUserSettings, setMapUserSettings] = useLocalStorageState<MapUserSettingsStructure>('map-user-settings', {
const [mapUserSettings, setMapUserSettings] = useLocalStorageState<MapUserSettingsStructure>(LS_KEY, {
defaultValue: EMPTY_OBJ,
});
@@ -101,18 +102,27 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
return;
}
if (mapUserSettings[map_slug] == null) {
const currentMapUserSettings = mapUserSettings[map_slug];
if (currentMapUserSettings == null) {
return;
}
const migratedResult = applyMigrations(map_slug, migrations);
try {
// INFO: after migrations migratedFromOld always will be true
const migratedResult = applyMigrations(
!currentMapUserSettings.migratedFromOld ? extractData(LS_KEY_LEGASY) : currentMapUserSettings,
);
if (!migratedResult) {
setIsReady(true);
return;
}
setMapUserSettings(migratedResult);
setMapUserSettings({ ...mapUserSettings, [map_slug]: migratedResult });
setIsReady(true);
} catch (error) {
setIsReady(true);
}
}, [isReady, mapUserSettings, map_slug, setMapUserSettings]);
const checkOldSettings = useCallback(() => {

View File

@@ -1,12 +1,7 @@
import { Dispatch, SetStateAction, useCallback, useMemo, useRef } from 'react';
import {
MapUserSettings,
MapUserSettingsStructure,
SettingsWithVersion,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import { MapUserSettings, MapUserSettingsStructure, SettingsWrapper } from '@/hooks/Mapper/mapRootProvider/types.ts';
type ExtractSettings<S extends keyof MapUserSettings> =
MapUserSettings[S] extends SettingsWithVersion<infer U> ? U : never;
type ExtractSettings<S extends keyof MapUserSettings> = MapUserSettings[S] extends SettingsWrapper<infer U> ? U : never;
type Setter<S extends keyof MapUserSettings> = (
value: Partial<ExtractSettings<S>> | ((prev: ExtractSettings<S>) => Partial<ExtractSettings<S>>),
@@ -25,21 +20,20 @@ export const useSettingsValueAndSetter = <S extends keyof MapUserSettings>(
if (!mapId) return {} as ExtractSettings<S>;
const mapSettings = settings[mapId];
return (mapSettings?.[setting]?.settings ?? ({} as ExtractSettings<S>)) as ExtractSettings<S>;
return (mapSettings?.[setting] ?? ({} as ExtractSettings<S>)) as ExtractSettings<S>;
}, [mapId, setting, settings]);
const refData = useRef({ mapId, setting, setSettings });
refData.current = { mapId, setting, setSettings };
const setter = useCallback<Setter<S>>((value, v) => {
const setter = useCallback<Setter<S>>(value => {
const { mapId, setting, setSettings } = refData.current;
if (!mapId) return;
setSettings(all => {
const currentMap = all[mapId];
const prev = currentMap[setting].settings as ExtractSettings<S>;
const version = v != null ? v : currentMap[setting].version;
const prev = currentMap[setting] as ExtractSettings<S>;
const patch =
typeof value === 'function' ? (value as (p: ExtractSettings<S>) => Partial<ExtractSettings<S>>)(prev) : value;
@@ -48,10 +42,7 @@ export const useSettingsValueAndSetter = <S extends keyof MapUserSettings>(
...all,
[mapId]: {
...currentMap,
[setting]: {
version,
settings: { ...(prev as any), ...patch } as ExtractSettings<S>,
},
[setting]: { ...(prev as any), ...patch } as ExtractSettings<S>,
},
};
});

View File

@@ -1,6 +0,0 @@
import { to_1 } from './to_1.ts';
import { to_2 } from './to_2.ts';
import { to_3 } from './to_3.ts';
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [to_1, to_2, to_3] as MigrationStructure[];

View File

@@ -1,13 +0,0 @@
import { MigrationStructure, MigrationTypes } from '@/hooks/Mapper/mapRootProvider/types.ts';
export const to_1: MigrationStructure = {
to: 1,
type: MigrationTypes.interface,
run: (prev: any) => {
return {
...prev,
test1: 'lol ke',
kek1: 'kek',
};
},
};

View File

@@ -1,14 +0,0 @@
import { MigrationStructure, MigrationTypes } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { omit } from '@/hooks/Mapper/utils/omit.ts';
export const to_2: MigrationStructure = {
to: 2,
type: MigrationTypes.interface,
run: (prev: any) => {
return {
...omit(prev, ['test1', 'kek1']),
test2: 'lol ke1',
kek2: 'kek1',
};
},
};

View File

@@ -1,14 +0,0 @@
import { MigrationStructure, MigrationTypes } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { omit } from '@/hooks/Mapper/utils/omit.ts';
export const to_3: MigrationStructure = {
to: 3,
type: MigrationTypes.interface,
run: (prev: any) => {
return {
...omit(prev, ['test2', 'kek2']),
test3: 'lol ke1333',
kek3: 'kek1333',
};
},
};

View File

@@ -1,7 +1,8 @@
import { MapUserSettingsStructure, MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { getDefaultSettingsByType } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
import { MapUserSettingsStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { STORED_SETTINGS_VERSION } from '@/hooks/Mapper/mapRootProvider/version.ts';
import { migrations } from '@/hooks/Mapper/mapRootProvider/migrations/index.ts';
const extractData = (localStoreKey = 'map-user-settings'): MapUserSettingsStructure | null => {
export const extractData = (localStoreKey = 'map-user-settings'): MapUserSettingsStructure | null => {
const val = localStorage.getItem(localStoreKey);
if (!val) {
return null;
@@ -10,38 +11,46 @@ const extractData = (localStoreKey = 'map-user-settings'): MapUserSettingsStruct
return JSON.parse(val);
};
export const applyMigrations = (
mapId: string,
migrations: MigrationStructure[],
localStoreKey = 'map-user-settings',
) => {
const currentLSData = extractData(localStoreKey);
export const applyMigrations = (mapSettings: any) => {
let currentMapSettings = { ...mapSettings };
// INFO if we have NO any data in store expected that we will use default
if (!currentLSData) {
if (!currentMapSettings) {
return;
}
const currentMapSettings = currentLSData[mapId];
for (const migration of migrations) {
const { to, run, type } = migration;
const currentValue = currentMapSettings[type];
if (!currentValue) {
currentMapSettings[type] = getDefaultSettingsByType(type);
continue;
const direction = STORED_SETTINGS_VERSION - (currentMapSettings.version || 0);
if (direction === 0) {
if (currentMapSettings.version == null) {
return { ...currentMapSettings, version: STORED_SETTINGS_VERSION, migratedFromOld: true };
}
// we skip if current version is older
if (currentValue.version > to) {
continue;
return;
}
const next = run(currentValue.settings);
currentMapSettings[type].version = to;
currentMapSettings[type].settings = next;
// Upgrade
if (direction > 0) {
const preparedMigrations = migrations.sort((a, b) => a.to - b.to).filter(x => x.to <= STORED_SETTINGS_VERSION);
for (const migration of preparedMigrations) {
const { to, up } = migration;
const next = up(currentMapSettings);
currentMapSettings = { ...next, version: to, migratedFromOld: true };
}
return currentLSData;
return currentMapSettings;
}
// DOWNGRADE
const preparedMigrations = migrations.sort((a, b) => b.to - a.to).filter(x => x.to - 1 >= STORED_SETTINGS_VERSION);
for (const migration of preparedMigrations) {
const { to, down } = migration;
const next = down(currentMapSettings);
currentMapSettings = { ...next, version: to - 1, migratedFromOld: true };
}
return currentMapSettings;
};

View File

@@ -1,18 +1,4 @@
import m_interface from './interface';
import killsWidget from './killsWidget';
import localWidget from './localWidget';
import onTheMap from './onTheMap';
import routes from './routes';
import signaturesWidget from './signaturesWidget';
import widgets from './widgets';
import list from './list';
export * from './applyMigrations.ts';
export const migrations = [
...m_interface,
...killsWidget,
...localWidget,
...onTheMap,
...routes,
...signaturesWidget,
...widgets,
];
export const migrations = [...list];

View File

@@ -1,3 +0,0 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [] as MigrationStructure[];

View File

@@ -1,3 +0,0 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [] as MigrationStructure[];

View File

@@ -0,0 +1,4 @@
import { to_1 } from './to_1.ts';
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [to_1 /*to_2, to_3*/] as MigrationStructure[];

View File

@@ -0,0 +1,15 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export const to_1: MigrationStructure = {
to: 1,
up: (prev: any) => {
return Object.keys(prev).reduce((acc, k) => {
return { ...acc, [k]: prev[k].settings };
}, Object.create(null));
},
down: (prev: any) => {
return Object.keys(prev).reduce((acc, k) => {
return { ...acc, [k]: { version: 0, settings: prev[k] } };
}, Object.create(null));
},
};

View File

@@ -1,3 +0,0 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [] as MigrationStructure[];

View File

@@ -1,3 +0,0 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [] as MigrationStructure[];

View File

@@ -1,3 +0,0 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [] as MigrationStructure[];

View File

@@ -1,3 +0,0 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [] as MigrationStructure[];

View File

@@ -1,3 +0,0 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [] as MigrationStructure[];

View File

@@ -64,19 +64,18 @@ export type KillsWidgetSettings = {
timeRange: number;
};
export type SettingsWithVersion<T> = {
version: number;
settings: T;
};
export type SettingsWrapper<T> = T;
export type MapUserSettings = {
widgets: SettingsWithVersion<WindowStoreInfo>;
interface: SettingsWithVersion<InterfaceStoredSettings>;
onTheMap: SettingsWithVersion<OnTheMapSettingsType>;
routes: SettingsWithVersion<RoutesType>;
localWidget: SettingsWithVersion<LocalWidgetSettings>;
signaturesWidget: SettingsWithVersion<SignatureSettingsType>;
killsWidget: SettingsWithVersion<KillsWidgetSettings>;
migratedFromOld: boolean;
version: number;
widgets: SettingsWrapper<WindowStoreInfo>;
interface: SettingsWrapper<InterfaceStoredSettings>;
onTheMap: SettingsWrapper<OnTheMapSettingsType>;
routes: SettingsWrapper<RoutesType>;
localWidget: SettingsWrapper<LocalWidgetSettings>;
signaturesWidget: SettingsWrapper<SignatureSettingsType>;
killsWidget: SettingsWrapper<KillsWidgetSettings>;
};
export type MapUserSettingsStructure = {
@@ -87,7 +86,7 @@ export type WdResponse<T> = T;
export type RemoteAdminSettingsResponse = { default_settings?: string };
export enum MigrationTypes {
export enum SettingsTypes {
killsWidget = 'killsWidget',
localWidget = 'localWidget',
widgets = 'widgets',
@@ -100,6 +99,6 @@ export enum MigrationTypes {
export type MigrationFunc = (prev: any) => any;
export type MigrationStructure = {
to: number;
type: MigrationTypes;
run: MigrationFunc;
up: MigrationFunc;
down: MigrationFunc;
};

View File

@@ -0,0 +1,4 @@
export const STORED_SETTINGS_VERSION = 1;
export const LS_KEY_LEGASY = 'map-user-settings';
export const LS_KEY = 'map-user-settings-v2';

View File

@@ -1,12 +0,0 @@
// TODO IF YOU BUMP VERSION YOU SHOULD ADD MIGRATION!!!
// CHECK assets/js/hooks/Mapper/mapRootProvider/migrations
// FOR EACH TYPE AND EACH VERSION OF SETTINGS SHOULD BE ADDED MIGRATION
export const SETTING_VERSIONS = {
interface: 0,
routes: 0,
localWidget: 0,
onTheMap: 0,
kills: 0,
widgets: 0,
signatures: 0,
};

View File

@@ -2,3 +2,4 @@ export * from './contextStore';
export * from './getQueryVariable';
export * from './loadTextFile';
export * from './saveToFile';
export * from './omit';