@sincpro/mobile 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/adapters/ReceiptExporter.adapter.js +10 -2
  2. package/dist/entrypoints/ui/common_provider.d.ts +1 -1
  3. package/dist/entrypoints/ui/common_provider.js +2 -2
  4. package/dist/entrypoints/ui/domain_switcher.d.ts +2 -2
  5. package/dist/framework/feature.d.ts +26 -0
  6. package/dist/framework/feature.js +36 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/infrastructure/interceptor/intercept.d.ts +7 -2
  10. package/dist/infrastructure/interceptor/intercept.js +5 -2
  11. package/dist/infrastructure/telemetry/tracing/tracing_hooks.d.ts +9 -6
  12. package/dist/infrastructure/telemetry/tracing/tracing_hooks.js +60 -37
  13. package/dist/ui/components/atoms/DebugBanner.d.ts +1 -1
  14. package/dist/ui/components/molecules/BluetoothDeviceSelectorModal.d.ts +1 -1
  15. package/dist/ui/components/molecules/BluetoothDeviceSelectorModal.js +8 -2
  16. package/dist/ui/components/molecules/DeadLetterErrorBlock.d.ts +1 -1
  17. package/dist/ui/components/molecules/EventTimelineItem.d.ts +1 -1
  18. package/dist/ui/components/molecules/GeoPermissionCard.d.ts +1 -1
  19. package/dist/ui/components/molecules/ProcessToastProvider.d.ts +1 -1
  20. package/dist/ui/components/molecules/TimeZoneSelector.d.ts +1 -1
  21. package/dist/ui/components/molecules/TimeZoneSelectorModal.d.ts +1 -1
  22. package/dist/ui/components/molecules/TimeZoneSelectorModal.js +5 -3
  23. package/dist/ui/components/organisms/BluetoothPrinterSelector.d.ts +1 -1
  24. package/dist/ui/components/organisms/BluetoothPrinterSelector.js +3 -0
  25. package/dist/ui/components/organisms/DatabaseInfoRow.d.ts +1 -1
  26. package/dist/ui/components/organisms/DeadLetterQueueRow.d.ts +2 -1
  27. package/dist/ui/components/organisms/EventRow.d.ts +1 -1
  28. package/dist/ui/components/organisms/InjectableWebView.d.ts +2 -2
  29. package/dist/ui/components/organisms/InjectableWebView.js +6 -3
  30. package/dist/ui/components/organisms/Receipt.d.ts +2 -1
  31. package/dist/ui/components/organisms/TableInfoInfoRow.d.ts +1 -1
  32. package/dist/ui/screens/database/database.context.d.ts +2 -2
  33. package/dist/ui/screens/database/database.context.js +9 -18
  34. package/dist/ui/screens/database/database.json_detail.d.ts +1 -1
  35. package/dist/ui/screens/database/database.list.d.ts +1 -1
  36. package/dist/ui/screens/database/database.table_rows.d.ts +1 -1
  37. package/dist/ui/screens/dead_letter_queue/dead_letter_queue.context.d.ts +2 -2
  38. package/dist/ui/screens/dead_letter_queue/dead_letter_queue.list.d.ts +1 -1
  39. package/dist/ui/screens/events/events.context.d.ts +2 -2
  40. package/dist/ui/screens/events/events.context.js +20 -25
  41. package/dist/ui/screens/events/events.list.d.ts +1 -1
  42. package/package.json +41 -35
@@ -3,6 +3,14 @@ import * as Sharing from "expo-sharing";
3
3
  import { PDFDocument } from "pdf-lib";
4
4
  import { Platform } from "react-native";
5
5
  import { captureRef } from "react-native-view-shot";
6
+ /**
7
+ * view-shot 6 types a target as a class instance or a host node. React Native
8
+ * 0.88 types `View` as a function, so the ref we already hold is the same
9
+ * object at runtime and a different type here.
10
+ */
11
+ function shotTarget(viewRef) {
12
+ return viewRef;
13
+ }
6
14
  const DEFAULT_CONFIG = {
7
15
  RECEIPT_WIDTH_MM: 72,
8
16
  MM_TO_POINTS: 2.83464567,
@@ -16,7 +24,7 @@ function generateFileName(baseName, extension = "pdf") {
16
24
  return `${sanitized}_${Date.now()}.${extension}`;
17
25
  }
18
26
  async function captureViewToTmpFile(viewRef, quality) {
19
- const tmpUri = await captureRef(viewRef, {
27
+ const tmpUri = await captureRef(shotTarget(viewRef), {
20
28
  format: "jpg",
21
29
  quality,
22
30
  result: "tmpfile",
@@ -67,7 +75,7 @@ async function captureViewToBase64(viewRef, quality) {
67
75
  if (!viewRef.current)
68
76
  return null;
69
77
  try {
70
- const base64 = await captureRef(viewRef, {
78
+ const base64 = await captureRef(shotTarget(viewRef), {
71
79
  format: "png",
72
80
  quality,
73
81
  result: "base64",
@@ -36,6 +36,6 @@ interface CommonProviderProps {
36
36
  lightTheme?: ThemeTokens;
37
37
  darkTheme?: ThemeTokens;
38
38
  }
39
- export declare function CommonProvider({ children, lightTheme, darkTheme }: CommonProviderProps): import("react/jsx-runtime").JSX.Element;
39
+ export declare function CommonProvider({ children, lightTheme, darkTheme }: CommonProviderProps): import("react").JSX.Element;
40
40
  export declare function useCommon(): ICommonContext;
41
41
  export {};
@@ -104,10 +104,10 @@ export function CommonProvider({ children, lightTheme, darkTheme }) {
104
104
  applyTheme(scheme);
105
105
  }, [applyTheme]);
106
106
  useEffect(() => {
107
- checkGeoPermission();
107
+ Promise.resolve().then(() => checkGeoPermission());
108
108
  const sub = AppState.addEventListener("change", (state) => {
109
109
  if (state === "active")
110
- checkGeoPermission();
110
+ Promise.resolve().then(() => checkGeoPermission());
111
111
  });
112
112
  return () => sub.remove();
113
113
  }, [checkGeoPermission]);
@@ -13,7 +13,7 @@ export declare function DomainSwitcherProvider(props: {
13
13
  apps: DomainApp[];
14
14
  initialDomain: string;
15
15
  children: ReactNode;
16
- }): import("react/jsx-runtime").JSX.Element;
16
+ }): import("react").JSX.Element;
17
17
  export declare function useDomainSwitcher(): DomainSwitcherValue;
18
- export declare function ActiveDomainApp(): import("react/jsx-runtime").JSX.Element | null;
18
+ export declare function ActiveDomainApp(): import("react").JSX.Element | null;
19
19
  export {};
@@ -0,0 +1,26 @@
1
+ /**
2
+ * One operation. What the context injects (a repository, an adapter) is `deps`,
3
+ * typed by the subclass.
4
+ *
5
+ * `new` returns a proxy: every public method of the subclass runs inside a span
6
+ * named `ClassName.method`. A name that starts with `_` does not. The author
7
+ * does not open a span. If tracing is not initialized, or the exporter fails,
8
+ * the method still runs.
9
+ *
10
+ * A subclass constructor calls `super(deps)` and does not return another object.
11
+ */
12
+ export declare class Feature<Deps extends object = Record<string, never>> {
13
+ protected readonly deps: Deps;
14
+ constructor(deps?: Deps);
15
+ }
16
+ /**
17
+ * Composes the `Feature`s of its own context. `features` is that set, and only
18
+ * that set: another context's feature is not on the type.
19
+ *
20
+ * `run` is the parent span. Each `this.features.*` call in that turn is a child
21
+ * of the same trace. It does not call another `ApplicationService`.
22
+ */
23
+ export declare class ApplicationService<TFeatures extends Record<string, Feature<Record<string, unknown>>>, Deps extends object = Record<string, never>> extends Feature<Deps> {
24
+ protected readonly features: TFeatures;
25
+ constructor(features: TFeatures, deps?: Deps);
26
+ }
@@ -0,0 +1,36 @@
1
+ import { interceptInstance } from "../infrastructure/interceptor/intercept";
2
+ import { tracingHooks } from "../infrastructure/telemetry/tracing/tracing_hooks";
3
+ /**
4
+ * One operation. What the context injects (a repository, an adapter) is `deps`,
5
+ * typed by the subclass.
6
+ *
7
+ * `new` returns a proxy: every public method of the subclass runs inside a span
8
+ * named `ClassName.method`. A name that starts with `_` does not. The author
9
+ * does not open a span. If tracing is not initialized, or the exporter fails,
10
+ * the method still runs.
11
+ *
12
+ * A subclass constructor calls `super(deps)` and does not return another object.
13
+ */
14
+ export class Feature {
15
+ deps;
16
+ constructor(deps) {
17
+ this.deps = deps ?? {};
18
+ return interceptInstance(this, tracingHooks(new.target.name), {
19
+ skip: (methodName) => methodName.startsWith("_"),
20
+ });
21
+ }
22
+ }
23
+ /**
24
+ * Composes the `Feature`s of its own context. `features` is that set, and only
25
+ * that set: another context's feature is not on the type.
26
+ *
27
+ * `run` is the parent span. Each `this.features.*` call in that turn is a child
28
+ * of the same trace. It does not call another `ApplicationService`.
29
+ */
30
+ export class ApplicationService extends Feature {
31
+ features;
32
+ constructor(features, deps) {
33
+ super(deps);
34
+ this.features = features;
35
+ }
36
+ }
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { BaseModule, baseModule } from "./framework/base_module";
11
11
  export type { AppConfig } from "./framework/createApp";
12
12
  export { createApp } from "./framework/createApp";
13
13
  export { DomainModule } from "./framework/domain_module";
14
+ export { ApplicationService, Feature } from "./framework/feature";
14
15
  export { Kernel } from "./framework/kernel";
15
16
  export { orchestrator } from "./framework/orchestrator";
16
17
  export { installGlobalErrorHandler } from "./infrastructure/ui/errorHandler";
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { createTheme } from "./entrypoints/ui/theme";
6
6
  export { BaseModule, baseModule } from "./framework/base_module";
7
7
  export { createApp } from "./framework/createApp";
8
8
  export { DomainModule } from "./framework/domain_module";
9
+ export { ApplicationService, Feature } from "./framework/feature";
9
10
  export { Kernel } from "./framework/kernel";
10
11
  export { orchestrator } from "./framework/orchestrator";
11
12
  export { installGlobalErrorHandler } from "./infrastructure/ui/errorHandler";
@@ -41,6 +41,10 @@ export declare function interceptClass<T extends new (...args: never[]) => objec
41
41
  * function's `.name` (or `"anonymous"` if unnamed).
42
42
  */
43
43
  export declare function interceptFunction<T extends (...args: never[]) => unknown>(fn: T, hooks: InterceptorHooks): T;
44
+ export type InstanceInterceptOptions = {
45
+ /** Method names for which the proxy returns the raw function. */
46
+ skip?: (methodName: string) => boolean;
47
+ };
44
48
  /**
45
49
  * Wraps a single live instance via Proxy without touching the class prototype.
46
50
  * Use for singletons exported directly (e.g. `dbCursor`, `distributionWorkflows`).
@@ -52,6 +56,7 @@ export declare function interceptFunction<T extends (...args: never[]) => unknow
52
56
  *
53
57
  * Hooks contract: `before` fires before the call; `after` fires on success with
54
58
  * the resolved value; `onError` fires on throw or rejection — the error is always
55
- * re-thrown after `onError` completes.
59
+ * re-thrown after `onError` completes. `skip` leaves a method unwrapped — a `_`
60
+ * helper must not open a span.
56
61
  */
57
- export declare function interceptInstance<T extends object>(instance: T, hooks: InterceptorHooks): T;
62
+ export declare function interceptInstance<T extends object>(instance: T, hooks: InterceptorHooks, options?: InstanceInterceptOptions): T;
@@ -96,9 +96,10 @@ export function interceptFunction(fn, hooks) {
96
96
  *
97
97
  * Hooks contract: `before` fires before the call; `after` fires on success with
98
98
  * the resolved value; `onError` fires on throw or rejection — the error is always
99
- * re-thrown after `onError` completes.
99
+ * re-thrown after `onError` completes. `skip` leaves a method unwrapped — a `_`
100
+ * helper must not open a span.
100
101
  */
101
- export function interceptInstance(instance, hooks) {
102
+ export function interceptInstance(instance, hooks, options) {
102
103
  const className = instance.constructor?.name ?? "Object";
103
104
  return new Proxy(instance, {
104
105
  get(target, prop, receiver) {
@@ -106,6 +107,8 @@ export function interceptInstance(instance, hooks) {
106
107
  if (typeof value !== "function" || typeof prop === "symbol") {
107
108
  return value;
108
109
  }
110
+ if (options?.skip?.(prop))
111
+ return value;
109
112
  return wrap(value, className, prop, hooks).bind(target);
110
113
  },
111
114
  });
@@ -1,10 +1,13 @@
1
- import type { InterceptorHooks } from "../../interceptor/intercept";
1
+ import type { InterceptorHooks } from "../../../infrastructure/interceptor/intercept";
2
2
  /**
3
- * Returns InterceptorHooks that record an OTel span for every intercepted
4
- * method call. Parent-child relationships are maintained through the framework
5
- * StackContextManager: the OTel context (with the active span) is stored as a
6
- * value inside our Context, pushed in `before` and popped in `after`/`onError`.
3
+ * A span per intercepted call. The active span lives in the framework context,
4
+ * so a nested call — an application service running a feature — is a child of
5
+ * the same trace.
7
6
  *
8
- * @param tracerName - instrumentation scope name, e.g. "sincpro.mobile.checkout"
7
+ * Tracing is optional. No provider, a missing SDK, or an exporter that throws:
8
+ * the hooks swallow it and the use case runs. `before` only records a push it
9
+ * actually made, so `after` never pops a context it did not push.
10
+ *
11
+ * @param tracerName - instrumentation scope, e.g. "sincpro.mobile.checkout"
9
12
  */
10
13
  export declare function tracingHooks(tracerName: string): InterceptorHooks;
@@ -1,59 +1,82 @@
1
1
  import { ROOT_CONTEXT as OTEL_ROOT, SpanStatusCode, trace } from "@opentelemetry/api";
2
- import { _getContextManager } from "../../context_manager/index";
3
- import { OTEL_CTX_KEY } from "../context_keys";
2
+ import { _getContextManager } from "../../../infrastructure/context_manager";
3
+ import { OTEL_CTX_KEY } from "../../../infrastructure/telemetry/context_keys";
4
4
  const _spanMap = new WeakMap();
5
5
  // Tracks whether `before` successfully pushed a context entry for this call.
6
6
  // Guards `after`/`onError` against popping a context they never pushed.
7
7
  const _pushedMap = new WeakSet();
8
8
  /**
9
- * Returns InterceptorHooks that record an OTel span for every intercepted
10
- * method call. Parent-child relationships are maintained through the framework
11
- * StackContextManager: the OTel context (with the active span) is stored as a
12
- * value inside our Context, pushed in `before` and popped in `after`/`onError`.
9
+ * A span per intercepted call. The active span lives in the framework context,
10
+ * so a nested call — an application service running a feature — is a child of
11
+ * the same trace.
13
12
  *
14
- * @param tracerName - instrumentation scope name, e.g. "sincpro.mobile.checkout"
13
+ * Tracing is optional. No provider, a missing SDK, or an exporter that throws:
14
+ * the hooks swallow it and the use case runs. `before` only records a push it
15
+ * actually made, so `after` never pops a context it did not push.
16
+ *
17
+ * @param tracerName - instrumentation scope, e.g. "sincpro.mobile.checkout"
15
18
  */
16
19
  export function tracingHooks(tracerName) {
17
20
  return {
18
21
  before(call) {
19
- const manager = _getContextManager();
20
- const frameworkCtx = manager.active();
21
- // Read the OTel context carried inside our framework context.
22
- // Falls back to OTel ROOT if tracing has not been initialized yet.
23
- const otelCtx = frameworkCtx.get(OTEL_CTX_KEY) ?? OTEL_ROOT;
24
- const spanName = call.className
25
- ? `${call.className}.${call.methodName}`
26
- : call.methodName;
27
- const span = trace.getTracer(tracerName).startSpan(spanName, undefined, otelCtx);
28
- _spanMap.set(call, span);
29
- // Store the updated OTel context (now containing this span) back into
30
- // the framework context and push it onto the stack so nested calls see
31
- // this span as their parent.
32
- const newOtelCtx = trace.setSpan(otelCtx, span);
33
- manager.push(frameworkCtx.set(OTEL_CTX_KEY, newOtelCtx));
34
- _pushedMap.add(call);
22
+ try {
23
+ const manager = _getContextManager();
24
+ const frameworkCtx = manager.active();
25
+ const otelCtx = frameworkCtx.get(OTEL_CTX_KEY) ?? OTEL_ROOT;
26
+ const spanName = call.className
27
+ ? `${call.className}.${call.methodName}`
28
+ : call.methodName;
29
+ const span = trace.getTracer(tracerName).startSpan(spanName, undefined, otelCtx);
30
+ _spanMap.set(call, span);
31
+ manager.push(frameworkCtx.set(OTEL_CTX_KEY, trace.setSpan(otelCtx, span)));
32
+ _pushedMap.add(call);
33
+ }
34
+ catch {
35
+ _spanMap.delete(call);
36
+ }
35
37
  },
36
38
  after(call) {
37
39
  const span = _spanMap.get(call);
38
- if (!span)
39
- return;
40
40
  _spanMap.delete(call);
41
- span.setStatus({ code: SpanStatusCode.OK });
42
- span.end();
43
- if (_pushedMap.delete(call))
44
- _getContextManager().pop();
41
+ try {
42
+ span?.setStatus({ code: SpanStatusCode.OK });
43
+ span?.end();
44
+ }
45
+ catch {
46
+ // The use case already finished. A failing exporter does not change that.
47
+ }
48
+ finally {
49
+ pop(call);
50
+ }
45
51
  },
46
52
  onError(call, error) {
47
53
  const span = _spanMap.get(call);
48
- if (!span)
49
- return;
50
54
  _spanMap.delete(call);
51
- if (error instanceof Error)
52
- span.recordException(error);
53
- span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
54
- span.end();
55
- if (_pushedMap.delete(call))
56
- _getContextManager().pop();
55
+ try {
56
+ if (error instanceof Error)
57
+ span?.recordException(error);
58
+ span?.setStatus({
59
+ code: SpanStatusCode.ERROR,
60
+ message: error instanceof Error ? error.message : undefined,
61
+ });
62
+ span?.end();
63
+ }
64
+ catch {
65
+ // Same as `after`: the business error is the one that propagates.
66
+ }
67
+ finally {
68
+ pop(call);
69
+ }
57
70
  },
58
71
  };
59
72
  }
73
+ function pop(call) {
74
+ if (!_pushedMap.delete(call))
75
+ return;
76
+ try {
77
+ _getContextManager().pop();
78
+ }
79
+ catch {
80
+ // The stack was already cleared. Nothing of this call is left on it.
81
+ }
82
+ }
@@ -1,2 +1,2 @@
1
- declare function DebugBanner(): import("react/jsx-runtime").JSX.Element | null;
1
+ declare function DebugBanner(): import("react").JSX.Element | null;
2
2
  export { DebugBanner };
@@ -4,5 +4,5 @@ interface BluetoothDeviceSelectorModalProps {
4
4
  onClose: () => void;
5
5
  onSelect: (device: BluetoothDevice) => void;
6
6
  }
7
- declare function BluetoothDeviceSelectorModal({ visible, onClose, onSelect, }: BluetoothDeviceSelectorModalProps): import("react/jsx-runtime").JSX.Element;
7
+ declare function BluetoothDeviceSelectorModal({ visible, onClose, onSelect, }: BluetoothDeviceSelectorModalProps): import("react").JSX.Element;
8
8
  export { BluetoothDeviceSelectorModal };
@@ -23,6 +23,13 @@ function BluetoothDeviceSelectorModal({ visible, onClose, onSelect, }) {
23
23
  const [loading, setLoading] = useState(false);
24
24
  const [selectedDevice, setSelectedDevice] = useState(null);
25
25
  const [connecting, setConnecting] = useState(false);
26
+ const [prevVisible, setPrevVisible] = useState(visible);
27
+ if (visible !== prevVisible) {
28
+ setPrevVisible(visible);
29
+ if (visible) {
30
+ setSelectedDevice(null);
31
+ }
32
+ }
26
33
  const loadDevices = useCallback(async () => {
27
34
  setLoading(true);
28
35
  try {
@@ -38,8 +45,7 @@ function BluetoothDeviceSelectorModal({ visible, onClose, onSelect, }) {
38
45
  }, []);
39
46
  useEffect(() => {
40
47
  if (visible) {
41
- loadDevices();
42
- setSelectedDevice(null);
48
+ Promise.resolve().then(() => loadDevices());
43
49
  }
44
50
  }, [visible, loadDevices]);
45
51
  async function handleConfirm() {
@@ -4,5 +4,5 @@ interface DeadLetterErrorBlockProps {
4
4
  onToggle: () => void;
5
5
  previewChars?: number;
6
6
  }
7
- declare function DeadLetterErrorBlock({ errorMessage, expanded, onToggle, previewChars, }: DeadLetterErrorBlockProps): import("react/jsx-runtime").JSX.Element | null;
7
+ declare function DeadLetterErrorBlock({ errorMessage, expanded, onToggle, previewChars, }: DeadLetterErrorBlockProps): import("react").JSX.Element | null;
8
8
  export default DeadLetterErrorBlock;
@@ -3,5 +3,5 @@ interface EventTimelineItemProps {
3
3
  event: DomainEvent;
4
4
  isLast?: boolean;
5
5
  }
6
- declare function EventTimelineItem({ event, isLast }: EventTimelineItemProps): import("react/jsx-runtime").JSX.Element;
6
+ declare function EventTimelineItem({ event, isLast }: EventTimelineItemProps): import("react").JSX.Element;
7
7
  export default EventTimelineItem;
@@ -4,5 +4,5 @@ interface GeoPermissionCardProps {
4
4
  loading?: boolean;
5
5
  handleRequestPermission?: () => void;
6
6
  }
7
- declare function GeoPermissionCard({ title, loading, handleRequestPermission, hasPermission, }: GeoPermissionCardProps): import("react/jsx-runtime").JSX.Element;
7
+ declare function GeoPermissionCard({ title, loading, handleRequestPermission, hasPermission, }: GeoPermissionCardProps): import("react").JSX.Element;
8
8
  export default GeoPermissionCard;
@@ -1,4 +1,4 @@
1
1
  import type { ReactNode } from "react";
2
2
  export declare function ProcessToastProvider({ children }: {
3
3
  children: ReactNode;
4
- }): import("react/jsx-runtime").JSX.Element;
4
+ }): import("react").JSX.Element;
@@ -3,5 +3,5 @@ interface TimeZoneSelectorProps {
3
3
  onSelect?: (tz: TimezoneLocale) => void;
4
4
  timezone?: string | null;
5
5
  }
6
- export declare function TimeZoneSelector({ onSelect, timezone }: TimeZoneSelectorProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function TimeZoneSelector({ onSelect, timezone }: TimeZoneSelectorProps): import("react").JSX.Element;
7
7
  export {};
@@ -5,5 +5,5 @@ interface TimeZoneSelectorModalProps {
5
5
  onSelect: (tz: TimezoneLocale) => void;
6
6
  currentTimezone?: string | null;
7
7
  }
8
- declare function TimeZoneSelectorModal({ visible, onClose, onSelect, currentTimezone, }: TimeZoneSelectorModalProps): import("react/jsx-runtime").JSX.Element;
8
+ declare function TimeZoneSelectorModal({ visible, onClose, onSelect, currentTimezone, }: TimeZoneSelectorModalProps): import("react").JSX.Element;
9
9
  export { TimeZoneSelectorModal };
@@ -4,7 +4,7 @@ import { Display } from "@sincpro/mobile-ui/Display";
4
4
  import { Form } from "@sincpro/mobile-ui/Form";
5
5
  import { useTheme } from "@sincpro/mobile-ui/theme";
6
6
  import { Typography } from "@sincpro/mobile-ui/Typography";
7
- import { useEffect, useMemo, useState } from "react";
7
+ import { useMemo, useState } from "react";
8
8
  import { TextInput, TouchableOpacity, View } from "react-native";
9
9
  const TIMEZONES_LOCALES = {
10
10
  bolivia: { timezone: "America/La_Paz", locale: "es-BO" },
@@ -27,12 +27,14 @@ function TimeZoneSelectorModal({ visible, onClose, onSelect, currentTimezone, })
27
27
  const theme = useTheme();
28
28
  const [query, setQuery] = useState("");
29
29
  const [selectedTimezone, setSelectedTimezone] = useState(currentTimezone || null);
30
- useEffect(() => {
30
+ const [prevVisible, setPrevVisible] = useState(visible);
31
+ if (visible !== prevVisible) {
32
+ setPrevVisible(visible);
31
33
  if (visible) {
32
34
  setQuery("");
33
35
  setSelectedTimezone(currentTimezone || null);
34
36
  }
35
- }, [visible]);
37
+ }
36
38
  const timezoneList = useMemo(() => {
37
39
  return Object.values(TIMEZONES_LOCALES).map((tz) => ({
38
40
  ...tz,
@@ -5,5 +5,5 @@ interface BluetoothPrinterSelectorProps {
5
5
  /** Render as section rows (no outer card) — use inside FormViewV2.Content.Section. */
6
6
  compact?: boolean;
7
7
  }
8
- declare function BluetoothPrinterSelector({ onPrinterSelected, onConnectionChange, compact, }: BluetoothPrinterSelectorProps): import("react/jsx-runtime").JSX.Element;
8
+ declare function BluetoothPrinterSelector({ onPrinterSelected, onConnectionChange, compact, }: BluetoothPrinterSelectorProps): import("react").JSX.Element;
9
9
  export { BluetoothPrinterSelector };
@@ -43,6 +43,9 @@ function BluetoothPrinterSelector({ onPrinterSelected, onConnectionChange, compa
43
43
  setIsConnected(connected);
44
44
  }, []);
45
45
  useEffect(() => {
46
+ // Async side effect on mount — checkStatus does sync setState internally,
47
+ // which is the documented React 19 pattern for on-mount async work.
48
+ // eslint-disable-next-line react-hooks/set-state-in-effect
46
49
  checkStatus();
47
50
  }, [checkStatus]);
48
51
  useEffect(() => {
@@ -7,5 +7,5 @@ interface DatabaseInfoRowProps {
7
7
  item: DatabaseInfoRowData;
8
8
  onPress?: () => void;
9
9
  }
10
- declare function DatabaseInfoRow({ item, onPress }: DatabaseInfoRowProps): import("react/jsx-runtime").JSX.Element;
10
+ declare function DatabaseInfoRow({ item, onPress }: DatabaseInfoRowProps): import("react").JSX.Element;
11
11
  export default DatabaseInfoRow;
@@ -1,7 +1,8 @@
1
1
  import { DomainEvent } from "../../../domain/event_sourcing";
2
2
  import { IRowItemProps } from "@sincpro/mobile-ui/views/types/IListView";
3
+ import React from "react";
3
4
  interface DeadLetterQueueRowProps extends IRowItemProps<DomainEvent> {
4
5
  onRetry: (event: DomainEvent) => Promise<void> | void;
5
6
  }
6
- declare function DeadLetterQueueRow({ item, onRetry }: DeadLetterQueueRowProps): import("react/jsx-runtime").JSX.Element | null;
7
+ declare function DeadLetterQueueRow({ item, onRetry }: DeadLetterQueueRowProps): React.JSX.Element | null;
7
8
  export default DeadLetterQueueRow;
@@ -3,5 +3,5 @@ interface EventRowProps {
3
3
  item: DomainEvent;
4
4
  onRetry: (event: DomainEvent) => Promise<void> | void;
5
5
  }
6
- declare function EventRow({ item, onRetry }: EventRowProps): import("react/jsx-runtime").JSX.Element | null;
6
+ declare function EventRow({ item, onRetry }: EventRowProps): import("react").JSX.Element | null;
7
7
  export default EventRow;
@@ -27,8 +27,8 @@ interface WebViewControlsProps {
27
27
  onGoForward: () => void;
28
28
  onReload: () => void;
29
29
  }
30
- declare function WebViewControls({ canGoBack: _canGoBack, canGoForward: _canGoForward, onGoBack: _onGoBack, onGoForward: _onGoForward, onReload: _onReload, }: WebViewControlsProps): import("react/jsx-runtime").JSX.Element;
31
- declare function InjectableWebView({ url, scripts, injectedJavaScript, onMessage, onNavigationChange, onLoadStart, onLoadEnd, onError, renderLoading, backgroundColor, reloadKey, hardRefreshKey, maxRetries, useDefaultCache, }: InjectableWebViewProps): import("react/jsx-runtime").JSX.Element;
30
+ declare function WebViewControls({ canGoBack: _canGoBack, canGoForward: _canGoForward, onGoBack: _onGoBack, onGoForward: _onGoForward, onReload: _onReload, }: WebViewControlsProps): import("react").JSX.Element;
31
+ declare function InjectableWebView({ url, scripts, injectedJavaScript, onMessage, onNavigationChange, onLoadStart, onLoadEnd, onError, renderLoading, backgroundColor, reloadKey, hardRefreshKey, maxRetries, useDefaultCache, }: InjectableWebViewProps): import("react").JSX.Element;
32
32
  declare namespace InjectableWebView {
33
33
  var Controls: typeof WebViewControls;
34
34
  }
@@ -4,15 +4,18 @@ import { webViewService } from "../../../services/webview.service";
4
4
  import { useToast } from "@sincpro/mobile-ui/Feedback";
5
5
  import Spinner from "@sincpro/mobile-ui/Feedback/Feedback.Spinner";
6
6
  import { useBottomInset } from "@sincpro/mobile-ui/layouts/BottomInset";
7
- import { useCallback, useEffect, useRef, useState } from "react";
7
+ import { useCallback, useEffect, useRef, useState, } from "react";
8
8
  import { BackHandler, StyleSheet, View } from "react-native";
9
9
  import { SafeAreaView } from "react-native-safe-area-context";
10
- import WebView from "react-native-webview";
10
+ import RawWebView from "react-native-webview";
11
+ // The library defaults its extra-props generic to `undefined`, and
12
+ // `WebViewProps & undefined` collapses to `never`. An empty record keeps the real props.
13
+ const WebView = RawWebView;
11
14
  function WebViewControls({ canGoBack: _canGoBack, canGoForward: _canGoForward, onGoBack: _onGoBack, onGoForward: _onGoForward, onReload: _onReload, }) {
12
15
  return _jsx(View, {});
13
16
  }
14
17
  function WebViewLoadingIndicator({ backgroundColor }) {
15
- return (_jsx(View, { style: [StyleSheet.absoluteFillObject, styles.loading, { backgroundColor }], children: _jsx(Spinner, { size: "large", text: "Cargando p\u00E1gina..." }) }));
18
+ return (_jsx(View, { style: [StyleSheet.absoluteFill, styles.loading, { backgroundColor }], children: _jsx(Spinner, { size: "large", text: "Cargando p\u00E1gina..." }) }));
16
19
  }
17
20
  function useWebView() {
18
21
  const toast = useToast();
@@ -1,4 +1,5 @@
1
1
  import { FormatterMap, LabelMap } from "../../../domain/receipt";
2
+ import React from "react";
2
3
  interface ReceiptProps {
3
4
  data: Record<string, any>;
4
5
  labels?: LabelMap;
@@ -7,5 +8,5 @@ interface ReceiptProps {
7
8
  formatters?: FormatterMap;
8
9
  emptyPlaceholder?: string;
9
10
  }
10
- declare function Receipt({ data, labels, order, strictOrder, formatters, emptyPlaceholder, }: ReceiptProps): import("react/jsx-runtime").JSX.Element;
11
+ declare function Receipt({ data, labels, order, strictOrder, formatters, emptyPlaceholder, }: ReceiptProps): React.JSX.Element;
11
12
  export default Receipt;
@@ -5,5 +5,5 @@ interface TableListItemProps {
5
5
  item: TableRowData;
6
6
  onPress?: () => void;
7
7
  }
8
- declare function TableInfoRow({ item, onPress }: TableListItemProps): import("react/jsx-runtime").JSX.Element;
8
+ declare function TableInfoRow({ item, onPress }: TableListItemProps): import("react").JSX.Element;
9
9
  export default TableInfoRow;
@@ -1,4 +1,4 @@
1
- import { ReactNode } from "react";
1
+ import React, { ReactNode } from "react";
2
2
  export declare enum EDatabaseView {
3
3
  TABLES_LIST = "TABLES_LIST",
4
4
  TABLE_ROWS = "TABLE_ROWS",
@@ -27,6 +27,6 @@ interface IDatabaseContext {
27
27
  }
28
28
  export declare function DatabaseProvider({ children }: {
29
29
  children: ReactNode;
30
- }): import("react/jsx-runtime").JSX.Element;
30
+ }): React.JSX.Element;
31
31
  export declare function useDatabase(): IDatabaseContext;
32
32
  export {};
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "nativewind/jsx-runtime";
2
2
  import { databaseTablesUseCase } from "../../../services/database_table.service";
3
3
  import { safeJsonParse, safeJsonStringify } from "../../../tools/utils/serializer";
4
- import { createContext, useCallback, useContext, useEffect, useState, } from "react";
4
+ import { createContext, useCallback, useContext, useMemo, useState, } from "react";
5
5
  export var EDatabaseView;
6
6
  (function (EDatabaseView) {
7
7
  EDatabaseView["TABLES_LIST"] = "TABLES_LIST";
@@ -13,7 +13,6 @@ export function DatabaseProvider({ children }) {
13
13
  const [currentView, setCurrentView] = useState(EDatabaseView.TABLES_LIST);
14
14
  const [tables, setTables] = useState([]);
15
15
  const [data, setData] = useState([]);
16
- const [filteredData, setFilteredData] = useState([]);
17
16
  const [isLoading, setIsLoading] = useState(false);
18
17
  const [selectedTable, setSelectedTable] = useState("");
19
18
  const [selectedRowJson, setSelectedRowJson] = useState("");
@@ -21,13 +20,11 @@ export function DatabaseProvider({ children }) {
21
20
  const [currentRowIndex, setCurrentRowIndex] = useState(0);
22
21
  const [previousSearchQuery, setPreviousSearchQuery] = useState("");
23
22
  const [isTransitioning, setIsTransitioning] = useState(false);
24
- const filterData = useCallback(() => {
25
- if (!searchQuery.trim()) {
26
- setFilteredData(data);
27
- return;
28
- }
23
+ const filteredData = useMemo(() => {
24
+ if (!searchQuery.trim())
25
+ return data;
29
26
  const query = searchQuery.toLowerCase().trim();
30
- const filtered = data.filter((row) => {
27
+ return data.filter((row) => {
31
28
  const parsedData = safeJsonParse(typeof row === "string" ? row : JSON.stringify(row));
32
29
  if (!parsedData)
33
30
  return false;
@@ -36,11 +33,7 @@ export function DatabaseProvider({ children }) {
36
33
  const name = parsedData.name?.toLowerCase() || "";
37
34
  return remoteId.includes(query) || uuid.includes(query) || name.includes(query);
38
35
  });
39
- setFilteredData(filtered);
40
36
  }, [data, searchQuery]);
41
- useEffect(() => {
42
- filterData();
43
- }, [filterData]);
44
37
  const loadTables = useCallback(async () => {
45
38
  setIsLoading(true);
46
39
  try {
@@ -121,12 +114,10 @@ export function DatabaseProvider({ children }) {
121
114
  }, [currentRowIndex, filteredData, isTransitioning]);
122
115
  const hasNextRow = currentRowIndex < filteredData.length - 1;
123
116
  const hasPreviousRow = currentRowIndex > 0;
124
- useEffect(() => {
125
- if (currentView === EDatabaseView.JSON_DETAIL && searchQuery !== previousSearchQuery) {
126
- goBackFromJson();
127
- setPreviousSearchQuery(searchQuery);
128
- }
129
- }, [searchQuery, previousSearchQuery, currentView, goBackFromJson]);
117
+ if (currentView === EDatabaseView.JSON_DETAIL && searchQuery !== previousSearchQuery) {
118
+ setPreviousSearchQuery(searchQuery);
119
+ goBackFromJson();
120
+ }
130
121
  return (_jsx(DatabaseContext.Provider, { value: {
131
122
  currentView,
132
123
  tables,
@@ -1,2 +1,2 @@
1
- declare function JsonDetailView(): import("react/jsx-runtime").JSX.Element;
1
+ declare function JsonDetailView(): import("react").JSX.Element;
2
2
  export default JsonDetailView;
@@ -1 +1 @@
1
- export default function DatabaseList(): import("react/jsx-runtime").JSX.Element;
1
+ export default function DatabaseList(): import("react").JSX.Element;
@@ -1,2 +1,2 @@
1
- declare function TableRowsView(): import("react/jsx-runtime").JSX.Element;
1
+ declare function TableRowsView(): import("react").JSX.Element;
2
2
  export default TableRowsView;
@@ -1,5 +1,5 @@
1
1
  import { DomainEvent } from "../../../domain/event_sourcing";
2
- import { ReactNode } from "react";
2
+ import React, { ReactNode } from "react";
3
3
  interface IDeadLetterQueueContext {
4
4
  events: DomainEvent[];
5
5
  enrichedEvents: DomainEvent[];
@@ -29,6 +29,6 @@ interface IDeadLetterQueueContext {
29
29
  }
30
30
  export declare function DeadLetterQueueProvider({ children }: {
31
31
  children: ReactNode;
32
- }): import("react/jsx-runtime").JSX.Element;
32
+ }): React.JSX.Element;
33
33
  export declare function useDeadLetterQueue(): IDeadLetterQueueContext;
34
34
  export {};
@@ -1,2 +1,2 @@
1
- declare function DeadLetterQueueList(): import("react/jsx-runtime").JSX.Element;
1
+ declare function DeadLetterQueueList(): import("react").JSX.Element;
2
2
  export default DeadLetterQueueList;
@@ -1,5 +1,5 @@
1
1
  import { DomainEvent } from "../../../domain/event_sourcing";
2
- import { ReactNode } from "react";
2
+ import React, { ReactNode } from "react";
3
3
  type EventFilter = "ALL" | "FAILED" | "ACKNOWLEDGED" | "PENDING";
4
4
  interface IEventsContext {
5
5
  events: DomainEvent[];
@@ -20,6 +20,6 @@ interface IEventsContext {
20
20
  export declare function EventsProvider({ children, pageSize, }: {
21
21
  children: ReactNode;
22
22
  pageSize?: number;
23
- }): import("react/jsx-runtime").JSX.Element;
23
+ }): React.JSX.Element;
24
24
  export declare function useEvents(): IEventsContext;
25
25
  export {};
@@ -1,66 +1,61 @@
1
1
  import { jsx as _jsx } from "nativewind/jsx-runtime";
2
2
  import { eventService } from "../../../services/event.service";
3
- import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
3
+ import { createContext, useCallback, useContext, useMemo, useState, } from "react";
4
4
  const EventsContext = createContext(null);
5
5
  export function EventsProvider({ children, pageSize = 25, }) {
6
6
  const [events, setEvents] = useState([]);
7
- const [pagedEvents, setPagedEvents] = useState([]);
8
7
  const [isLoading, setIsLoading] = useState(false);
9
8
  const [isLoadingMore, setIsLoadingMore] = useState(false);
10
9
  const [isRetrying, setIsRetrying] = useState(false);
11
10
  const [page, setPage] = useState(1);
12
- const [hasMore, setHasMore] = useState(false);
13
11
  const [filter, setFilter] = useState("ALL");
14
- const filterEvents = useCallback((list, currentFilter) => {
15
- if (currentFilter === "ALL")
16
- return list;
17
- return list.filter((ev) => {
12
+ const filteredEvents = useMemo(() => {
13
+ if (filter === "ALL")
14
+ return events;
15
+ return events.filter((ev) => {
18
16
  if (!ev.status)
19
17
  return false;
20
- if (currentFilter === "FAILED")
18
+ if (filter === "FAILED")
21
19
  return ev.status === "FAILED";
22
- if (currentFilter === "ACKNOWLEDGED")
20
+ if (filter === "ACKNOWLEDGED")
23
21
  return ev.status === "ACKNOWLEDGED";
24
- if (currentFilter === "PENDING")
22
+ if (filter === "PENDING")
25
23
  return ev.status === "PENDING";
26
24
  return true;
27
25
  });
28
- }, []);
29
- const paginate = useCallback((list, nextPage, currentFilter) => {
30
- const filtered = filterEvents(list, currentFilter);
31
- const slice = filtered.slice(0, nextPage * pageSize);
32
- setPagedEvents(slice);
33
- setHasMore(slice.length < filtered.length);
34
- setPage(nextPage);
35
- }, [filterEvents, pageSize]);
26
+ }, [events, filter]);
27
+ const pagedEvents = useMemo(() => filteredEvents.slice(0, page * pageSize), [filteredEvents, page, pageSize]);
28
+ const hasMore = pagedEvents.length < filteredEvents.length;
29
+ const [prevFilter, setPrevFilter] = useState(filter);
30
+ if (filter !== prevFilter) {
31
+ setPrevFilter(filter);
32
+ setPage(1);
33
+ }
36
34
  const loadEvents = useCallback(async () => {
37
35
  setIsLoading(true);
38
36
  try {
39
37
  const fetched = await eventService.listAllEvents();
40
38
  setEvents(fetched);
41
- paginate(fetched, 1, filter);
39
+ setPage(1);
42
40
  }
43
41
  finally {
44
42
  setIsLoading(false);
45
43
  }
46
- }, [filter, paginate]);
44
+ }, []);
47
45
  const refreshEvents = useCallback(async () => {
48
46
  await loadEvents();
49
47
  }, [loadEvents]);
50
- useEffect(() => {
51
- paginate(events, 1, filter);
52
- }, [events, filter, paginate]);
53
48
  const loadMore = useCallback(async () => {
54
49
  if (isLoadingMore || isLoading || !hasMore)
55
50
  return;
56
51
  setIsLoadingMore(true);
57
52
  try {
58
- paginate(events, page + 1, filter);
53
+ setPage((p) => p + 1);
59
54
  }
60
55
  finally {
61
56
  setIsLoadingMore(false);
62
57
  }
63
- }, [events, hasMore, isLoading, isLoadingMore, page, paginate, filter]);
58
+ }, [hasMore, isLoading, isLoadingMore]);
64
59
  const retryEvent = useCallback(async (event) => {
65
60
  setIsRetrying(true);
66
61
  try {
@@ -1 +1 @@
1
- export default function EventsScreen(): import("react/jsx-runtime").JSX.Element;
1
+ export default function EventsScreen(): import("react").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sincpro/mobile",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Sincpro Mobile framework core: offline-first infra (DB, queue/events, cron), DDD patterns, adapters, AppShell and module-driven composition for React Native",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -26,18 +26,21 @@
26
26
  "repository": "https://github.com/Sincpro-SRL/sincpro_mobile",
27
27
  "author": "Gutierrez, Andres <andres.gutierrez@sincpro.com.bo>",
28
28
  "license": "MIT",
29
+ "engines": {
30
+ "node": ">=26"
31
+ },
29
32
  "dependencies": {
30
- "@opentelemetry/api": "^1.9.0",
31
- "@opentelemetry/core": "^2.8.0",
32
- "@opentelemetry/resources": "^2.8.0",
33
- "@opentelemetry/sdk-trace-base": "^2.8.0",
33
+ "@opentelemetry/api": "^1.9.1",
34
+ "@opentelemetry/core": "^2.10.0",
35
+ "@opentelemetry/resources": "^2.10.0",
36
+ "@opentelemetry/sdk-trace-base": "^2.10.0",
34
37
  "async-mutex": "^0.5.0",
35
38
  "big.js": "^7.0.1",
36
- "dayjs": "^1.11.18",
39
+ "dayjs": "^1.11.21",
37
40
  "mitt": "^3.0.1",
38
41
  "pdf-lib": "^1.17.1",
39
42
  "tailwind-variants": "^3.2.2",
40
- "uuid": "^13.0.0"
43
+ "uuid": "^14.0.1"
41
44
  },
42
45
  "peerDependencies": {
43
46
  "@react-navigation/native": ">=7",
@@ -56,7 +59,7 @@
56
59
  "expo-splash-screen": "*",
57
60
  "expo-sqlite": "*",
58
61
  "expo-task-manager": "*",
59
- "nativewind": ">=4",
62
+ "nativewind": ">=4.2.7",
60
63
  "react": "*",
61
64
  "react-native": "*",
62
65
  "react-native-gesture-handler": "*",
@@ -65,7 +68,8 @@
65
68
  "react-native-screens": "*",
66
69
  "react-native-toast-message": "*",
67
70
  "react-native-view-shot": "*",
68
- "react-native-webview": "*"
71
+ "react-native-webview": "*",
72
+ "react-native-worklets": "*"
69
73
  },
70
74
  "devDependencies": {
71
75
  "@opentelemetry/api": "^1.9.0",
@@ -73,40 +77,42 @@
73
77
  "@react-navigation/native-stack": "^7.0.0",
74
78
  "@sincpro/mobile-ui": "^0.7.2",
75
79
  "@types/big.js": "^6.2.2",
76
- "@types/react": "~19.1.10",
80
+ "@types/react": "~19.3.0",
77
81
  "@types/uuid": "^10.0.0",
78
82
  "eslint": "^9.35.0",
79
- "eslint-config-expo": "~10.0.0",
83
+ "eslint-config-expo": "~58.0.4",
80
84
  "eslint-config-prettier": "^10.1.8",
81
85
  "eslint-plugin-prettier": "^5.5.4",
82
86
  "eslint-plugin-react": "^7.37.2",
83
87
  "eslint-plugin-simple-import-sort": "^12.1.1",
84
- "expo-background-task": "~1.0.10",
85
- "expo-clipboard": "~8.0.8",
86
- "expo-crypto": "~15.0.9",
87
- "expo-file-system": "~19.0.23",
88
- "expo-linking": "~8.0.12",
89
- "expo-localization": "~17.0.9",
90
- "expo-location": "~19.0.8",
91
- "expo-network": "~8.0.8",
92
- "expo-print": "~15.0.8",
93
- "expo-sharing": "~14.0.8",
94
- "expo-splash-screen": "~31.0.13",
95
- "expo-sqlite": "~16.0.10",
96
- "expo-task-manager": "~14.0.9",
97
- "nativewind": "^4.0.1",
88
+ "expo-background-task": "~58.0.12",
89
+ "expo-clipboard": "~58.0.4",
90
+ "expo-crypto": "~58.0.7",
91
+ "expo-file-system": "~58.0.13",
92
+ "expo-linking": "~58.0.16",
93
+ "expo-localization": "~58.0.5",
94
+ "expo-location": "~58.0.20",
95
+ "expo-network": "~58.0.4",
96
+ "expo-print": "~58.0.8",
97
+ "expo-sharing": "~58.0.22",
98
+ "expo-splash-screen": "~58.0.13",
99
+ "expo-sqlite": "~58.0.15",
100
+ "expo-task-manager": "~58.0.13",
101
+ "nativewind": "4.2.7",
102
+ "tailwindcss": "3.4.19",
98
103
  "prettier": "^3.6.2",
99
- "react": "19.1.0",
100
- "react-native": "0.81.5",
101
- "react-native-gesture-handler": "~2.28.0",
102
- "react-native-keyboard-controller": "1.18.5",
103
- "react-native-reanimated": "~4.1.1",
104
- "react-native-safe-area-context": "~5.6.0",
105
- "react-native-screens": "~4.25.2",
106
- "react-native-svg": "15.12.1",
104
+ "react": "19.3.0",
105
+ "react-native": "0.88.1",
106
+ "react-native-gesture-handler": "~3.3.0",
107
+ "react-native-keyboard-controller": "1.22.7",
108
+ "react-native-reanimated": "4.7.1",
109
+ "react-native-safe-area-context": "~5.11.1",
110
+ "react-native-screens": "~4.30.1",
111
+ "react-native-svg": "15.16.0",
107
112
  "react-native-toast-message": "^2.3.3",
108
- "react-native-view-shot": "^4.0.3",
109
- "react-native-webview": "13.15.0",
113
+ "react-native-view-shot": "6.1.1",
114
+ "react-native-webview": "14.0.1",
115
+ "react-native-worklets": "0.13.0",
110
116
  "tsc-alias": "^1.8.17",
111
117
  "tsx": "^4.22.4",
112
118
  "typescript": "~5.9.2"