From d3b574934767f31bc7fa9161c991003e11249842 Mon Sep 17 00:00:00 2001 From: kuraydev Date: Mon, 29 Jun 2026 16:11:54 +0300 Subject: [PATCH 1/6] fix(ai): make streaming work on-device with XHR SSE transport React Native's fetch has no streaming ReadableStream body, so every provider's response.body.getReader() threw 'No response body' and no tokens ever arrived. Add an XMLHttpRequest-based SSE transport with a cross-chunk line buffer (SSEParser) so tokens split across network packets are no longer dropped, and route all three providers through it. Also: type the OpenAI/Anthropic/Gemini wire formats (drop 'as any'), map stream errors to AIError with statusCode, honor the documented (but previously ignored) AIConfig.systemPrompt, and let Gemini respect config.baseURL. Public callback/contract shapes are unchanged. --- src/hooks/useAIChat.ts | 7 +- src/services/ai/AIService.ts | 22 +++- src/services/ai/index.ts | 11 +- src/services/ai/providers/anthropic.ts | 144 +++++++++++------------- src/services/ai/providers/gemini.ts | 148 ++++++++++++------------- src/services/ai/providers/openai.ts | 112 +++++++++---------- src/services/ai/sse.ts | 136 +++++++++++++++++++++++ src/services/ai/types.ts | 42 +++++++ 8 files changed, 401 insertions(+), 221 deletions(-) create mode 100644 src/services/ai/sse.ts diff --git a/src/hooks/useAIChat.ts b/src/hooks/useAIChat.ts index e5490aa..986f8b4 100644 --- a/src/hooks/useAIChat.ts +++ b/src/hooks/useAIChat.ts @@ -71,11 +71,8 @@ export function useAIChat({ setError(null); try { - const mergedConfig: AIConfig = config.systemPrompt - ? { ...config } - : config; - - const response = await sendAIMessage(updatedMessages, mergedConfig); + // config.systemPrompt is honored centrally by sendAIMessage. + const response = await sendAIMessage(updatedMessages, config); setMessages((prev) => [...prev, response.message]); onResponse?.(response); } catch (err) { diff --git a/src/services/ai/AIService.ts b/src/services/ai/AIService.ts index 95419ed..89359f4 100644 --- a/src/services/ai/AIService.ts +++ b/src/services/ai/AIService.ts @@ -43,7 +43,7 @@ export function sendAIMessage( config: AIConfig, ): Promise { const provider = createProvider(config.provider); - return provider.sendMessage(messages, config); + return provider.sendMessage(withSystemPrompt(messages, config), config); } /** @@ -62,7 +62,25 @@ export function streamAIMessage( callbacks: AIStreamCallbacks, ): Promise { const provider = createProvider(config.provider); - return provider.streamMessage(messages, config, callbacks); + return provider.streamMessage( + withSystemPrompt(messages, config), + config, + callbacks, + ); +} + +/** + * Honor {@link AIConfig.systemPrompt} by prepending a system message when the + * caller supplied a prompt and the conversation doesn't already contain one. + * Centralised here so every provider receives a consistent message list. + */ +function withSystemPrompt( + messages: AIMessage[], + config: AIConfig, +): AIMessage[] { + if (!config.systemPrompt) return messages; + if (messages.some((m) => m.role === "system")) return messages; + return [buildSystemMessage(config.systemPrompt), ...messages]; } /** diff --git a/src/services/ai/index.ts b/src/services/ai/index.ts index ad5e7c3..2df284f 100644 --- a/src/services/ai/index.ts +++ b/src/services/ai/index.ts @@ -5,7 +5,16 @@ export { buildUserMessage, } from "./AIService"; -export { AI_PROVIDER_LABELS, AI_BASE_URLS, AIError } from "./types"; +export { + AI_PROVIDER_LABELS, + AI_BASE_URLS, + AIError, + extractApiErrorMessage, + toAIError, +} from "./types"; + +export { SSEError, SSEParser, streamSSE } from "./sse"; +export type { StreamSSEOptions } from "./sse"; export type { AIProvider, diff --git a/src/services/ai/providers/anthropic.ts b/src/services/ai/providers/anthropic.ts index 5bb6cf1..ae25eaf 100644 --- a/src/services/ai/providers/anthropic.ts +++ b/src/services/ai/providers/anthropic.ts @@ -1,5 +1,7 @@ +import { streamSSE } from "../sse"; import { AIError, + toAIError, type AIChatResponse, type AIConfig, type AIMessage, @@ -7,10 +9,25 @@ import { type IAIProvider, } from "../types"; +// ─── Wire types ───────────────────────────────────────────────────────────── + interface AnthropicErrorBody { error?: { message?: string }; } +interface AnthropicResponseBody { + id?: string; + content?: Array<{ text?: string }>; + stop_reason?: string; + usage?: { input_tokens: number; output_tokens: number }; +} + +interface AnthropicStreamEvent { + type?: string; + delta?: { type?: string; text?: string }; +} + +const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"; const ANTHROPIC_VERSION = "2023-06-01"; export class AnthropicProvider implements IAIProvider { @@ -18,27 +35,12 @@ export class AnthropicProvider implements IAIProvider { messages: AIMessage[], config: AIConfig, ): Promise { - const baseURL = config.baseURL ?? "https://api.anthropic.com/v1"; - - // Anthropic keeps system prompt separate from the messages array - const systemMessage = messages.find((m) => m.role === "system"); - const conversation = messages - .filter((m) => m.role !== "system") - .map(({ role, content }) => ({ role, content })); + const baseURL = config.baseURL ?? DEFAULT_BASE_URL; const response = await fetch(`${baseURL}/messages`, { method: "POST", - headers: { - "Content-Type": "application/json", - "x-api-key": config.apiKey, - "anthropic-version": ANTHROPIC_VERSION, - }, - body: JSON.stringify({ - model: config.model, - max_tokens: config.maxTokens ?? 1024, - ...(systemMessage ? { system: systemMessage.content } : {}), - messages: conversation, - }), + headers: this.headers(config), + body: JSON.stringify(this.buildBody(messages, config, false)), }); if (!response.ok) { @@ -52,27 +54,24 @@ export class AnthropicProvider implements IAIProvider { ); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const data = (await response.json()) as any; - const content = (data.content?.[0]?.text ?? "") as string; + const data = (await response.json()) as AnthropicResponseBody; + const content = data.content?.[0]?.text ?? ""; return { message: { - id: (data.id ?? `anthropic-${Date.now()}`) as string, + id: data.id ?? `anthropic-${Date.now()}`, role: "assistant", content, timestamp: Date.now(), }, usage: data.usage ? { - promptTokens: data.usage.input_tokens as number, - completionTokens: data.usage.output_tokens as number, - totalTokens: - (data.usage.input_tokens as number) + - (data.usage.output_tokens as number), + promptTokens: data.usage.input_tokens, + completionTokens: data.usage.output_tokens, + totalTokens: data.usage.input_tokens + data.usage.output_tokens, } : undefined, - finishReason: data.stop_reason as string | undefined, + finishReason: data.stop_reason, }; } @@ -81,60 +80,22 @@ export class AnthropicProvider implements IAIProvider { config: AIConfig, callbacks: AIStreamCallbacks, ): Promise { - const baseURL = config.baseURL ?? "https://api.anthropic.com/v1"; - - const systemMessage = messages.find((m) => m.role === "system"); - const conversation = messages - .filter((m) => m.role !== "system") - .map(({ role, content }) => ({ role, content })); + const baseURL = config.baseURL ?? DEFAULT_BASE_URL; + let fullContent = ""; try { - const response = await fetch(`${baseURL}/messages`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-api-key": config.apiKey, - "anthropic-version": ANTHROPIC_VERSION, - }, - body: JSON.stringify({ - model: config.model, - max_tokens: config.maxTokens ?? 1024, - ...(systemMessage ? { system: systemMessage.content } : {}), - messages: conversation, - stream: true, - }), - }); - - if (!response.ok) { - throw new AIError( - `Anthropic stream request failed (${response.status})`, - "anthropic", - response.status, - ); - } - - const reader = response.body?.getReader(); - if (!reader) throw new AIError("No response body", "anthropic"); - - const decoder = new TextDecoder(); - let fullContent = ""; - - for (;;) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value, { stream: true }); - const lines = chunk.split("\n").filter((l) => l.startsWith("data: ")); - - for (const line of lines) { + await streamSSE({ + url: `${baseURL}/messages`, + headers: this.headers(config), + body: JSON.stringify(this.buildBody(messages, config, true)), + onData: (data) => { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const json = JSON.parse(line.slice(6)) as any; + const json = JSON.parse(data) as AnthropicStreamEvent; if ( json.type === "content_block_delta" && json.delta?.type === "text_delta" ) { - const token = (json.delta.text ?? "") as string; + const token = json.delta.text ?? ""; if (token) { fullContent += token; callbacks.onToken?.(token); @@ -143,8 +104,8 @@ export class AnthropicProvider implements IAIProvider { } catch { // skip malformed SSE chunks } - } - } + }, + }); callbacks.onComplete?.({ message: { @@ -155,8 +116,33 @@ export class AnthropicProvider implements IAIProvider { }, }); } catch (error) { - const e = error instanceof Error ? error : new Error(String(error)); - callbacks.onError?.(e); + callbacks.onError?.( + toAIError(error, "anthropic", "Anthropic stream failed"), + ); } } + + private headers(config: AIConfig): Record { + return { + "Content-Type": "application/json", + "x-api-key": config.apiKey, + "anthropic-version": ANTHROPIC_VERSION, + }; + } + + private buildBody(messages: AIMessage[], config: AIConfig, stream: boolean) { + // Anthropic keeps the system prompt separate from the messages array. + const systemMessage = messages.find((m) => m.role === "system"); + const conversation = messages + .filter((m) => m.role !== "system") + .map(({ role, content }) => ({ role, content })); + + return { + model: config.model, + max_tokens: config.maxTokens ?? 1024, + ...(systemMessage ? { system: systemMessage.content } : {}), + messages: conversation, + ...(stream ? { stream: true } : {}), + }; + } } diff --git a/src/services/ai/providers/gemini.ts b/src/services/ai/providers/gemini.ts index 7531b67..cace205 100644 --- a/src/services/ai/providers/gemini.ts +++ b/src/services/ai/providers/gemini.ts @@ -1,5 +1,7 @@ +import { streamSSE } from "../sse"; import { AIError, + toAIError, type AIChatResponse, type AIConfig, type AIMessage, @@ -7,46 +9,34 @@ import { type IAIProvider, } from "../types"; +// ─── Wire types ───────────────────────────────────────────────────────────── + interface GeminiErrorBody { error?: { message?: string }; } -export class GeminiProvider implements IAIProvider { - private buildContents(messages: AIMessage[]) { - return messages - .filter((m) => m.role !== "system") - .map(({ role, content }) => ({ - // Gemini uses "model" instead of "assistant" - role: role === "assistant" ? "model" : "user", - parts: [{ text: content }], - })); - } +interface GeminiCandidate { + content?: { parts?: Array<{ text?: string }> }; + finishReason?: string; +} - private buildRequestBody(messages: AIMessage[], config: AIConfig) { - const systemMessage = messages.find((m) => m.role === "system"); - return { - contents: this.buildContents(messages), - ...(systemMessage - ? { - system_instruction: { - parts: [{ text: systemMessage.content }], - }, - } - : {}), - generationConfig: { - temperature: config.temperature ?? 0.7, - maxOutputTokens: config.maxTokens ?? 1024, - }, - }; - } +interface GeminiResponseBody { + candidates?: GeminiCandidate[]; + usageMetadata?: { + promptTokenCount?: number; + candidatesTokenCount?: number; + totalTokenCount?: number; + }; +} +const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"; + +export class GeminiProvider implements IAIProvider { async sendMessage( messages: AIMessage[], config: AIConfig, ): Promise { - const geminiBase = - "https://generativelanguage.googleapis.com/v1beta/models"; - const url = `${geminiBase}/${config.model}:generateContent?key=${config.apiKey}`; + const url = `${this.modelsURL(config)}/${config.model}:generateContent?key=${config.apiKey}`; const response = await fetch(url, { method: "POST", @@ -63,10 +53,9 @@ export class GeminiProvider implements IAIProvider { ); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const data = (await response.json()) as any; - const content = (data.candidates?.[0]?.content?.parts?.[0]?.text ?? - "") as string; + const data = (await response.json()) as GeminiResponseBody; + const candidate = data.candidates?.[0]; + const content = candidate?.content?.parts?.[0]?.text ?? ""; return { message: { @@ -77,13 +66,12 @@ export class GeminiProvider implements IAIProvider { }, usage: data.usageMetadata ? { - promptTokens: (data.usageMetadata.promptTokenCount ?? 0) as number, - completionTokens: (data.usageMetadata.candidatesTokenCount ?? - 0) as number, - totalTokens: (data.usageMetadata.totalTokenCount ?? 0) as number, + promptTokens: data.usageMetadata.promptTokenCount ?? 0, + completionTokens: data.usageMetadata.candidatesTokenCount ?? 0, + totalTokens: data.usageMetadata.totalTokenCount ?? 0, } : undefined, - finishReason: data.candidates?.[0]?.finishReason as string | undefined, + finishReason: candidate?.finishReason, }; } @@ -92,45 +80,19 @@ export class GeminiProvider implements IAIProvider { config: AIConfig, callbacks: AIStreamCallbacks, ): Promise { - // streamGenerateContent returns server-sent events - const geminiBase = - "https://generativelanguage.googleapis.com/v1beta/models"; - const url = `${geminiBase}/${config.model}:streamGenerateContent?alt=sse&key=${config.apiKey}`; + // streamGenerateContent returns server-sent events when alt=sse is set. + const url = `${this.modelsURL(config)}/${config.model}:streamGenerateContent?alt=sse&key=${config.apiKey}`; + let fullContent = ""; try { - const response = await fetch(url, { - method: "POST", + await streamSSE({ + url, headers: { "Content-Type": "application/json" }, body: JSON.stringify(this.buildRequestBody(messages, config)), - }); - - if (!response.ok) { - throw new AIError( - `Gemini stream request failed (${response.status})`, - "gemini", - response.status, - ); - } - - const reader = response.body?.getReader(); - if (!reader) throw new AIError("No response body", "gemini"); - - const decoder = new TextDecoder(); - let fullContent = ""; - - for (;;) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value, { stream: true }); - const lines = chunk.split("\n").filter((l) => l.startsWith("data: ")); - - for (const line of lines) { + onData: (data) => { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const json = JSON.parse(line.slice(6)) as any; - const token = (json.candidates?.[0]?.content?.parts?.[0]?.text ?? - "") as string; + const json = JSON.parse(data) as GeminiResponseBody; + const token = json.candidates?.[0]?.content?.parts?.[0]?.text ?? ""; if (token) { fullContent += token; callbacks.onToken?.(token); @@ -138,8 +100,8 @@ export class GeminiProvider implements IAIProvider { } catch { // skip malformed SSE chunks } - } - } + }, + }); callbacks.onComplete?.({ message: { @@ -150,8 +112,40 @@ export class GeminiProvider implements IAIProvider { }, }); } catch (error) { - const e = error instanceof Error ? error : new Error(String(error)); - callbacks.onError?.(e); + callbacks.onError?.(toAIError(error, "gemini", "Gemini stream failed")); } } + + private modelsURL(config: AIConfig): string { + const baseURL = config.baseURL ?? DEFAULT_BASE_URL; + return `${baseURL}/models`; + } + + private buildContents(messages: AIMessage[]) { + return messages + .filter((m) => m.role !== "system") + .map(({ role, content }) => ({ + // Gemini uses "model" instead of "assistant" + role: role === "assistant" ? "model" : "user", + parts: [{ text: content }], + })); + } + + private buildRequestBody(messages: AIMessage[], config: AIConfig) { + const systemMessage = messages.find((m) => m.role === "system"); + return { + contents: this.buildContents(messages), + ...(systemMessage + ? { + system_instruction: { + parts: [{ text: systemMessage.content }], + }, + } + : {}), + generationConfig: { + temperature: config.temperature ?? 0.7, + maxOutputTokens: config.maxTokens ?? 1024, + }, + }; + } } diff --git a/src/services/ai/providers/openai.ts b/src/services/ai/providers/openai.ts index 9d57b6f..7d686da 100644 --- a/src/services/ai/providers/openai.ts +++ b/src/services/ai/providers/openai.ts @@ -1,5 +1,7 @@ +import { streamSSE } from "../sse"; import { AIError, + toAIError, type AIChatResponse, type AIConfig, type AIMessage, @@ -7,16 +9,39 @@ import { type IAIProvider, } from "../types"; +// ─── Wire types ───────────────────────────────────────────────────────────── + interface OpenAIErrorBody { error?: { message?: string }; } +interface OpenAIChoice { + message?: { content?: string }; + finish_reason?: string; +} + +interface OpenAIResponseBody { + id?: string; + choices: OpenAIChoice[]; + usage?: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; +} + +interface OpenAIStreamChunk { + choices?: Array<{ delta?: { content?: string } }>; +} + +const DEFAULT_BASE_URL = "https://api.openai.com/v1"; + export class OpenAIProvider implements IAIProvider { async sendMessage( messages: AIMessage[], config: AIConfig, ): Promise { - const baseURL = config.baseURL ?? "https://api.openai.com/v1"; + const baseURL = config.baseURL ?? DEFAULT_BASE_URL; const response = await fetch(`${baseURL}/chat/completions`, { method: "POST", @@ -24,12 +49,7 @@ export class OpenAIProvider implements IAIProvider { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}`, }, - body: JSON.stringify({ - model: config.model, - messages: messages.map(({ role, content }) => ({ role, content })), - temperature: config.temperature ?? 0.7, - max_tokens: config.maxTokens ?? 1024, - }), + body: JSON.stringify(this.buildBody(messages, config, false)), }); if (!response.ok) { @@ -41,25 +61,24 @@ export class OpenAIProvider implements IAIProvider { ); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const data = (await response.json()) as any; + const data = (await response.json()) as OpenAIResponseBody; const [choice] = data.choices; return { message: { id: data.id ?? `openai-${Date.now()}`, role: "assistant", - content: (choice.message?.content ?? "") as string, + content: choice?.message?.content ?? "", timestamp: Date.now(), }, usage: data.usage ? { - promptTokens: data.usage.prompt_tokens as number, - completionTokens: data.usage.completion_tokens as number, - totalTokens: data.usage.total_tokens as number, + promptTokens: data.usage.prompt_tokens, + completionTokens: data.usage.completion_tokens, + totalTokens: data.usage.total_tokens, } : undefined, - finishReason: choice.finish_reason as string | undefined, + finishReason: choice?.finish_reason, }; } @@ -68,52 +87,22 @@ export class OpenAIProvider implements IAIProvider { config: AIConfig, callbacks: AIStreamCallbacks, ): Promise { - const baseURL = config.baseURL ?? "https://api.openai.com/v1"; + const baseURL = config.baseURL ?? DEFAULT_BASE_URL; + let fullContent = ""; try { - const response = await fetch(`${baseURL}/chat/completions`, { - method: "POST", + await streamSSE({ + url: `${baseURL}/chat/completions`, headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}`, }, - body: JSON.stringify({ - model: config.model, - messages: messages.map(({ role, content }) => ({ role, content })), - temperature: config.temperature ?? 0.7, - max_tokens: config.maxTokens ?? 1024, - stream: true, - }), - }); - - if (!response.ok) { - throw new AIError( - `OpenAI stream request failed (${response.status})`, - "openai", - response.status, - ); - } - - const reader = response.body?.getReader(); - if (!reader) throw new AIError("No response body", "openai"); - - const decoder = new TextDecoder(); - let fullContent = ""; - - for (;;) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value, { stream: true }); - const lines = chunk.split("\n").filter((l) => l.startsWith("data: ")); - - for (const line of lines) { - const raw = line.slice(6).trim(); - if (raw === "[DONE]") continue; + body: JSON.stringify(this.buildBody(messages, config, true)), + onData: (data) => { + if (data === "[DONE]") return; try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const json = JSON.parse(raw) as any; - const token = (json.choices?.[0]?.delta?.content ?? "") as string; + const json = JSON.parse(data) as OpenAIStreamChunk; + const token = json.choices?.[0]?.delta?.content ?? ""; if (token) { fullContent += token; callbacks.onToken?.(token); @@ -121,8 +110,8 @@ export class OpenAIProvider implements IAIProvider { } catch { // skip malformed SSE chunks } - } - } + }, + }); callbacks.onComplete?.({ message: { @@ -133,8 +122,17 @@ export class OpenAIProvider implements IAIProvider { }, }); } catch (error) { - const e = error instanceof Error ? error : new Error(String(error)); - callbacks.onError?.(e); + callbacks.onError?.(toAIError(error, "openai", "OpenAI stream failed")); } } + + private buildBody(messages: AIMessage[], config: AIConfig, stream: boolean) { + return { + model: config.model, + messages: messages.map(({ role, content }) => ({ role, content })), + temperature: config.temperature ?? 0.7, + max_tokens: config.maxTokens ?? 1024, + ...(stream ? { stream: true } : {}), + }; + } } diff --git a/src/services/ai/sse.ts b/src/services/ai/sse.ts new file mode 100644 index 0000000..aef6c9e --- /dev/null +++ b/src/services/ai/sse.ts @@ -0,0 +1,136 @@ +/** + * React Native compatible Server-Sent Events (SSE) transport. + * + * Why this exists: React Native's `fetch` does **not** expose a streaming + * `ReadableStream` body — `response.body` is `undefined` on a device/simulator, + * so the common `response.body.getReader()` pattern throws immediately and no + * tokens ever arrive. `XMLHttpRequest`, by contrast, fires `onprogress` with an + * ever-growing `responseText` on React Native, which lets us read an SSE stream + * incrementally without any native module. + * + * `SSEParser` is the pure, transport-agnostic line buffer. It is the piece that + * fixes the "token split across two network chunks gets dropped" bug: it holds + * an internal buffer and only emits a `data:` payload once a full line (`\n`) + * has been received, carrying partial lines across `feed()` calls. + */ + +// ─── Errors ───────────────────────────────────────────────────────────────── + +export class SSEError extends Error { + constructor( + message: string, + public readonly status?: number, + public readonly responseText?: string, + ) { + super(message); + this.name = "SSEError"; + } +} + +// ─── Parser ─────────────────────────────────────────────────────────────────── + +/** + * Incremental SSE line parser. Feed it raw text chunks as they arrive; it + * returns the `data:` payloads for every *complete* line seen so far and keeps + * any trailing partial line buffered for the next `feed()`. + */ +export class SSEParser { + private buffer = ""; + + /** Push a raw chunk; returns the `data:` payloads for newly completed lines. */ + feed(chunk: string): string[] { + this.buffer += chunk; + const payloads: string[] = []; + let newlineIndex = this.buffer.indexOf("\n"); + while (newlineIndex !== -1) { + const line = this.buffer.slice(0, newlineIndex); + this.buffer = this.buffer.slice(newlineIndex + 1); + const payload = extractData(line); + if (payload !== null) payloads.push(payload); + newlineIndex = this.buffer.indexOf("\n"); + } + return payloads; + } + + /** Emit any `data:` payload left in a trailing line without a final newline. */ + flush(): string[] { + const remaining = this.buffer; + this.buffer = ""; + const payload = extractData(remaining); + return payload !== null ? [payload] : []; + } +} + +/** Strip the `data:` prefix (and one optional leading space) per the SSE spec. */ +function extractData(rawLine: string): string | null { + const line = rawLine.replace(/\r$/, ""); + if (!line.startsWith("data:")) return null; + const value = line.slice(5); + return value.startsWith(" ") ? value.slice(1) : value; +} + +// ─── Transport ──────────────────────────────────────────────────────────────── + +export interface StreamSSEOptions { + url: string; + method?: string; + headers: Record; + body: string; + /** Called once for every complete `data:` payload (excluding the prefix). */ + onData: (data: string) => void; +} + +/** + * POST `body` to `url` and stream the SSE response, invoking `onData` for each + * `data:` payload. Resolves when the stream ends, rejects (with `SSEError` + * carrying the HTTP status + body) on an HTTP error or network failure. + */ +export function streamSSE(options: StreamSSEOptions): Promise { + const { url, method = "POST", headers, body, onData } = options; + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + const parser = new SSEParser(); + let processed = 0; + + const pump = () => { + const { responseText } = xhr; + if (responseText.length <= processed) return; + const fresh = responseText.slice(processed); + processed = responseText.length; + for (const data of parser.feed(fresh)) onData(data); + }; + + xhr.open(method, url); + for (const [key, value] of Object.entries(headers)) { + xhr.setRequestHeader(key, value); + } + + xhr.onprogress = () => { + // Defer HTTP-error handling to onload, where we have the full body. + if (xhr.status >= 400) return; + pump(); + }; + + xhr.onload = () => { + if (xhr.status >= 400) { + reject( + new SSEError( + `Request failed (${xhr.status})`, + xhr.status, + xhr.responseText, + ), + ); + return; + } + pump(); + for (const data of parser.flush()) onData(data); + resolve(); + }; + + xhr.onerror = () => reject(new SSEError("Network request failed")); + xhr.ontimeout = () => reject(new SSEError("Request timed out")); + + xhr.send(body); + }); +} diff --git a/src/services/ai/types.ts b/src/services/ai/types.ts index be378cb..a6c4375 100644 --- a/src/services/ai/types.ts +++ b/src/services/ai/types.ts @@ -1,3 +1,5 @@ +import { SSEError } from "./sse"; + // ─── Core Types ─────────────────────────────────────────────────────────────── export type AIProvider = "openai" | "anthropic" | "gemini"; @@ -85,3 +87,43 @@ export class AIError extends Error { this.name = "AIError"; } } + +/** Pull a human-readable message out of a provider's JSON error body. */ +export function extractApiErrorMessage( + responseText?: string, +): string | undefined { + if (!responseText) return undefined; + try { + const parsed = JSON.parse(responseText) as { + error?: { message?: string } | string; + }; + if (typeof parsed.error === "string") return parsed.error; + return parsed.error?.message; + } catch { + return undefined; + } +} + +/** + * Normalise any thrown value (including {@link SSEError}) into an {@link AIError} + * so stream callbacks always receive a typed error carrying `provider` and, + * where available, the HTTP `statusCode`. + */ +export function toAIError( + error: unknown, + provider: AIProvider, + fallbackMessage: string, +): AIError { + if (error instanceof AIError) return error; + if (error instanceof SSEError) { + const message = + extractApiErrorMessage(error.responseText) ?? + error.message ?? + fallbackMessage; + return new AIError(message, provider, error.status); + } + if (error instanceof Error) { + return new AIError(error.message || fallbackMessage, provider); + } + return new AIError(fallbackMessage, provider); +} From b4b57c00dd236c5f139e5a7d4d6c1543137957a9 Mon Sep 17 00:00:00 2001 From: kuraydev Date: Mon, 29 Jun 2026 16:12:07 +0300 Subject: [PATCH 2/6] fix(ts): working event emitter, resolvable aliases, clean typecheck gate - EventEmitter no longer imports Node's 'events' (not polyfilled by Metro and not installed); replace with a dependency-free impl, same public API. - Add missing tsconfig paths (@models, @event-emitter, bare @theme/@services/ @screens/@shared-components) and real stub folders for @api/@local-storage so every documented alias type-resolves. - Fix the tsconfig vs RN-base moduleResolution conflict (node -> bundler) that made tsc --noEmit error before it ever reached source. - Move *.png ambient module declarations to assets.d.ts so they stay global pattern-ambient modules (@assets/logo.png now type-resolves). - Drop 'route: any' and 'useEffect((): any =>' smells; scope LogBox down from ignoreAllLogs() to an explicit list. --- App.tsx | 5 +- assets.d.ts | 31 ++++++++++ global.d.ts | 12 +--- src/navigation/index.tsx | 10 ++-- src/services/api/index.ts | 16 ++++++ src/services/event-emitter/index.ts | 88 ++++++++++++++++++++++++++++- src/services/local-storage/index.ts | 17 ++++++ tsconfig.json | 12 +++- 8 files changed, 172 insertions(+), 19 deletions(-) create mode 100644 assets.d.ts create mode 100644 src/services/api/index.ts create mode 100644 src/services/local-storage/index.ts diff --git a/App.tsx b/App.tsx index 2845c15..841d160 100644 --- a/App.tsx +++ b/App.tsx @@ -8,7 +8,10 @@ import { isAndroid } from "@freakycoder/react-native-helpers"; */ import Navigation from "./src/navigation"; -LogBox.ignoreAllLogs(); +// Scope log suppression to known-benign third-party warnings only. Avoid +// LogBox.ignoreAllLogs() — it hides real bugs during development. Add specific +// message prefixes here as you encounter noisy, non-actionable warnings. +LogBox.ignoreLogs([]); const App = () => { const scheme = useColorScheme(); diff --git a/assets.d.ts b/assets.d.ts new file mode 100644 index 0000000..4305a9f --- /dev/null +++ b/assets.d.ts @@ -0,0 +1,31 @@ +/** + * Ambient declarations for static asset imports (e.g. `import logo from + * "@assets/logo.png"`). This file intentionally has no top-level import/export + * so it stays a *global* script — pattern ambient modules like `*.png` are only + * treated as global when declared outside a module. + */ + +declare module "*.png" { + const value: number; + export default value; +} + +declare module "*.jpg" { + const value: number; + export default value; +} + +declare module "*.jpeg" { + const value: number; + export default value; +} + +declare module "*.gif" { + const value: number; + export default value; +} + +declare module "*.webp" { + const value: number; + export default value; +} diff --git a/global.d.ts b/global.d.ts index 97a7961..92e9142 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,14 +1,8 @@ import { theme } from "./src/shared/theme/themes"; -declare module "*.png" { - const value: number; - export default value; -} - -declare module "@assets/*.png" { - const value: number; - export default value; -} +// Static asset (*.png / *.jpg / …) ambient module declarations live in +// assets.d.ts so they stay global pattern-ambient modules (this file is a +// module because of the import above, which would scope them). declare module "@react-navigation/native" { export type ExtendedTheme = typeof theme; diff --git a/src/navigation/index.tsx b/src/navigation/index.tsx index f150cf0..7a9f279 100644 --- a/src/navigation/index.tsx +++ b/src/navigation/index.tsx @@ -26,12 +26,14 @@ const Navigation = () => { const scheme = useColorScheme(); const isDarkMode = scheme === "dark"; - React.useEffect((): any => { - return () => (isReadyRef.current = false); + React.useEffect(() => { + return () => { + isReadyRef.current = false; + }; }, []); const renderTabIcon = ( - route: any, + route: { name: string }, focused: boolean, color: string, size: number, @@ -106,7 +108,7 @@ const Navigation = () => { - {(props) => } + {() => } diff --git a/src/services/api/index.ts b/src/services/api/index.ts new file mode 100644 index 0000000..fce301f --- /dev/null +++ b/src/services/api/index.ts @@ -0,0 +1,16 @@ +/** + * API service stub. + * + * This is an intentional placeholder so the documented `@api` path alias + * resolves out of the box. Drop your axios instance / API client here. + * + * @example + * import axios from "axios"; + * + * export const api = axios.create({ + * baseURL: "https://api.example.com", + * timeout: 15000, + * }); + */ + +export {}; diff --git a/src/services/event-emitter/index.ts b/src/services/event-emitter/index.ts index 6cd4670..6c57dbb 100644 --- a/src/services/event-emitter/index.ts +++ b/src/services/event-emitter/index.ts @@ -1,7 +1,89 @@ -import { EventEmitter } from "events"; +/** + * Minimal, dependency-free EventEmitter singleton. + * + * Previously this imported Node's `events` module, which React Native's Metro + * bundler does not polyfill by default — the package isn't installed, so the + * import failed and the documented `@event-emitter` feature never worked on a + * device. This self-contained implementation keeps the exact same public API + * (`emit` / `on` / `off` / `once` / `removeAllListeners`) with no native or + * Node dependency. + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Listener = (...args: any[]) => void; + +export class EventEmitter { + private readonly listeners = new Map>(); + private maxListeners = 10; + + setMaxListeners(max: number): this { + this.maxListeners = max; + return this; + } + + on(event: string, listener: Listener): this { + const set = this.listeners.get(event) ?? new Set(); + set.add(listener); + this.listeners.set(event, set); + if (set.size > this.maxListeners) { + // eslint-disable-next-line no-console + console.warn( + `[EventEmitter] "${event}" now has ${set.size} listeners ` + + `(max ${this.maxListeners}); possible memory leak.`, + ); + } + return this; + } + + addListener(event: string, listener: Listener): this { + return this.on(event, listener); + } + + once(event: string, listener: Listener): this { + const wrapper: Listener = (...args) => { + this.off(event, wrapper); + listener(...args); + }; + return this.on(event, wrapper); + } + + off(event: string, listener: Listener): this { + const set = this.listeners.get(event); + if (set) { + set.delete(listener); + if (set.size === 0) this.listeners.delete(event); + } + return this; + } + + removeListener(event: string, listener: Listener): this { + return this.off(event, listener); + } + + removeAllListeners(event?: string): this { + if (event === undefined) { + this.listeners.clear(); + } else { + this.listeners.delete(event); + } + return this; + } + + listenerCount(event: string): number { + return this.listeners.get(event)?.size ?? 0; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + emit(event: string, ...args: any[]): boolean { + const set = this.listeners.get(event); + if (!set || set.size === 0) return false; + // Copy so listeners removing themselves mid-emit don't skip siblings. + for (const listener of [...set]) listener(...args); + return true; + } +} -// Initialization const eventEmitter = new EventEmitter(); eventEmitter.setMaxListeners(50); -// Export the module + export default eventEmitter; diff --git a/src/services/local-storage/index.ts b/src/services/local-storage/index.ts new file mode 100644 index 0000000..88956a7 --- /dev/null +++ b/src/services/local-storage/index.ts @@ -0,0 +1,17 @@ +/** + * Local storage service stub. + * + * This is an intentional placeholder so the documented `@local-storage` path + * alias resolves out of the box. Wire up your persistence layer of choice here + * (e.g. `@react-native-async-storage/async-storage` or `react-native-mmkv`). + * + * @example + * import AsyncStorage from "@react-native-async-storage/async-storage"; + * + * export const setItem = (key: string, value: string) => + * AsyncStorage.setItem(key, value); + * + * export const getItem = (key: string) => AsyncStorage.getItem(key); + */ + +export {}; diff --git a/tsconfig.json b/tsconfig.json index ca2c3b7..368ba40 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "noEmit": true, "isolatedModules": false, "strict": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "esModuleInterop": true, // ? Custom ones @@ -29,18 +29,26 @@ // ? Babel Plugin Module Resolver "baseUrl": "./src", "paths": { + "@shared-components": ["./shared/components"], "@shared-components/*": ["./shared/components/*"], "@shared-constants": ["./shared/constants"], "@font-size": ["./shared/theme/font-size"], "@api": ["./services/api/index"], "@fonts": ["./shared/theme/fonts"], "@colors": ["./shared/theme/colors"], + "@theme": ["./shared/theme"], "@theme/*": ["./shared/theme/*"], + "@models": ["./services/models"], + "@services": ["./services"], "@services/*": ["./services/*"], + "@screens": ["./screens"], "@screens/*": ["./screens/*"], "@utils": ["./utils/"], "@assets": ["./assets/"], - "@assets/*": ["./assets/*"], + // Note: `@assets/.png` imports are typed by the ambient `*.png` + // module declarations in global.d.ts, not by a path glob (a glob here + // would shadow those declarations and break asset type resolution). + "@event-emitter": ["./services/event-emitter"], "@local-storage": ["./services/local-storage"], "@hooks": ["./hooks/index"], "@hooks/*": ["./hooks/*"], From 619188b77cf19576e3d593c06ac02607b2bdfdc7 Mon Sep 17 00:00:00 2001 From: kuraydev Date: Mon, 29 Jun 2026 16:12:18 +0300 Subject: [PATCH 3/6] test: add AI service, hooks and event-emitter suites Cover the SSE line buffer, all three providers (request shaping, response parsing, error mapping, streaming via a mock XHR), the service layer + system-prompt injection, the useAIChat/useAICompletion hooks, and the event emitter. Add a jest setup (native module mocks + gesture-handler/ reanimated) so the App smoke test runs, scope testMatch to *.test.*, and fix transformIgnorePatterns for the ESM-published deps. 46 tests, 9 suites. --- __tests__/App.test.tsx | 20 +++- __tests__/ai/AIService.test.ts | 101 ++++++++++++++++ __tests__/ai/anthropic.test.ts | 113 ++++++++++++++++++ __tests__/ai/gemini.test.ts | 104 +++++++++++++++++ __tests__/ai/openai.test.ts | 142 +++++++++++++++++++++++ __tests__/ai/sse.test.ts | 71 ++++++++++++ __tests__/event-emitter.test.ts | 66 +++++++++++ __tests__/helpers/mockXhr.ts | 75 ++++++++++++ __tests__/helpers/renderHook.tsx | 35 ++++++ __tests__/hooks/useAIChat.test.tsx | 119 +++++++++++++++++++ __tests__/hooks/useAICompletion.test.tsx | 76 ++++++++++++ jest.config.js | 9 ++ jest.setup.js | 38 ++++++ 13 files changed, 966 insertions(+), 3 deletions(-) create mode 100644 __tests__/ai/AIService.test.ts create mode 100644 __tests__/ai/anthropic.test.ts create mode 100644 __tests__/ai/gemini.test.ts create mode 100644 __tests__/ai/openai.test.ts create mode 100644 __tests__/ai/sse.test.ts create mode 100644 __tests__/event-emitter.test.ts create mode 100644 __tests__/helpers/mockXhr.ts create mode 100644 __tests__/helpers/renderHook.tsx create mode 100644 __tests__/hooks/useAIChat.test.tsx create mode 100644 __tests__/hooks/useAICompletion.test.tsx create mode 100644 jest.setup.js diff --git a/__tests__/App.test.tsx b/__tests__/App.test.tsx index e532f70..ff15ec6 100644 --- a/__tests__/App.test.tsx +++ b/__tests__/App.test.tsx @@ -6,8 +6,22 @@ import React from 'react'; import ReactTestRenderer from 'react-test-renderer'; import App from '../App'; -test('renders correctly', async () => { - await ReactTestRenderer.act(() => { - ReactTestRenderer.create(); +// The stack navigator schedules a card-transition animation via setTimeout. +// Under real timers that callback fires *after* the test finishes and Jest has +// torn the environment down, which crashes the worker. Fake timers keep that +// pending animation from ever running on the real clock. +jest.useFakeTimers(); + +test('renders without crashing', async () => { + let tree: ReactTestRenderer.ReactTestRenderer | undefined; + + await ReactTestRenderer.act(async () => { + tree = ReactTestRenderer.create(); + }); + + expect(tree).toBeDefined(); + + await ReactTestRenderer.act(async () => { + tree?.unmount(); }); }); diff --git a/__tests__/ai/AIService.test.ts b/__tests__/ai/AIService.test.ts new file mode 100644 index 0000000..e844d41 --- /dev/null +++ b/__tests__/ai/AIService.test.ts @@ -0,0 +1,101 @@ +import { + buildSystemMessage, + buildUserMessage, + sendAIMessage, +} from "../../src/services/ai/AIService"; +import { extractApiErrorMessage } from "../../src/services/ai/types"; +import type { AIConfig, AIMessage } from "../../src/services/ai/types"; + +function okResponse(body: unknown) { + return { ok: true, status: 200, json: async () => body } as Response; +} + +describe("message builders", () => { + it("buildUserMessage produces a user message with content + id + timestamp", () => { + const msg = buildUserMessage("hello"); + expect(msg.role).toBe("user"); + expect(msg.content).toBe("hello"); + expect(typeof msg.id).toBe("string"); + expect(typeof msg.timestamp).toBe("number"); + }); + + it("buildSystemMessage produces a system message", () => { + const msg = buildSystemMessage("be nice"); + expect(msg.role).toBe("system"); + expect(msg.content).toBe("be nice"); + }); +}); + +describe("sendAIMessage system-prompt injection", () => { + const base: AIConfig = { + provider: "openai", + apiKey: "sk", + model: "gpt-4o-mini", + }; + const userOnly: AIMessage[] = [ + { id: "1", role: "user", content: "Hi", timestamp: 0 }, + ]; + + afterEach(() => jest.restoreAllMocks()); + + it("prepends config.systemPrompt as a system message when none exists", async () => { + const fetchMock = jest + .fn() + .mockResolvedValue(okResponse({ choices: [{ message: { content: "" } }] })); + globalThis.fetch = fetchMock as typeof fetch; + + await sendAIMessage(userOnly, { ...base, systemPrompt: "You are a bot." }); + + const sent = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(sent.messages[0]).toMatchObject({ + role: "system", + content: "You are a bot.", + }); + expect(sent.messages[1]).toMatchObject({ role: "user", content: "Hi" }); + }); + + it("does not double-inject when a system message is already present", async () => { + const fetchMock = jest + .fn() + .mockResolvedValue(okResponse({ choices: [{ message: { content: "" } }] })); + globalThis.fetch = fetchMock as typeof fetch; + + const withSystem: AIMessage[] = [ + { id: "s", role: "system", content: "Existing", timestamp: 0 }, + ...userOnly, + ]; + await sendAIMessage(withSystem, { ...base, systemPrompt: "Ignored" }); + + const sent = JSON.parse(fetchMock.mock.calls[0][1].body); + const systemMsgs = sent.messages.filter( + (m: AIMessage) => m.role === "system", + ); + expect(systemMsgs).toHaveLength(1); + expect(systemMsgs[0].content).toBe("Existing"); + }); + + it("leaves messages untouched when no systemPrompt is set", async () => { + const fetchMock = jest + .fn() + .mockResolvedValue(okResponse({ choices: [{ message: { content: "" } }] })); + globalThis.fetch = fetchMock as typeof fetch; + + await sendAIMessage(userOnly, base); + + const sent = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(sent.messages).toHaveLength(1); + }); +}); + +describe("extractApiErrorMessage", () => { + it("reads { error: { message } }", () => { + expect(extractApiErrorMessage('{"error":{"message":"boom"}}')).toBe("boom"); + }); + it("reads a string error field", () => { + expect(extractApiErrorMessage('{"error":"nope"}')).toBe("nope"); + }); + it("returns undefined for non-JSON or empty input", () => { + expect(extractApiErrorMessage("not json")).toBeUndefined(); + expect(extractApiErrorMessage(undefined)).toBeUndefined(); + }); +}); diff --git a/__tests__/ai/anthropic.test.ts b/__tests__/ai/anthropic.test.ts new file mode 100644 index 0000000..a244695 --- /dev/null +++ b/__tests__/ai/anthropic.test.ts @@ -0,0 +1,113 @@ +import { AnthropicProvider } from "../../src/services/ai/providers/anthropic"; +import { AIError, type AIConfig, type AIMessage } from "../../src/services/ai/types"; +import { installMockXHR, lastXHR } from "../helpers/mockXhr"; + +function okResponse(body: unknown) { + return { ok: true, status: 200, json: async () => body } as Response; +} + +const config: AIConfig = { + provider: "anthropic", + apiKey: "ak-test", + model: "claude-3-5-haiku", +}; + +const messages: AIMessage[] = [ + { id: "s", role: "system", content: "Be terse.", timestamp: 0 }, + { id: "1", role: "user", content: "Hi", timestamp: 0 }, +]; + +describe("AnthropicProvider.sendMessage", () => { + afterEach(() => jest.restoreAllMocks()); + + it("separates the system prompt and sets the version header", async () => { + const fetchMock = jest.fn().mockResolvedValue( + okResponse({ + id: "msg_1", + content: [{ text: "Hello" }], + stop_reason: "end_turn", + usage: { input_tokens: 4, output_tokens: 1 }, + }), + ); + globalThis.fetch = fetchMock as typeof fetch; + + const result = await new AnthropicProvider().sendMessage(messages, config); + + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toBe("https://api.anthropic.com/v1/messages"); + expect(init.headers["x-api-key"]).toBe("ak-test"); + expect(init.headers["anthropic-version"]).toBe("2023-06-01"); + + const sent = JSON.parse(init.body); + expect(sent.system).toBe("Be terse."); + expect(sent.messages).toEqual([{ role: "user", content: "Hi" }]); + expect(sent.messages.some((m: AIMessage) => m.role === "system")).toBe( + false, + ); + + expect(result.message.content).toBe("Hello"); + expect(result.usage?.totalTokens).toBe(5); + expect(result.finishReason).toBe("end_turn"); + }); + + it("omits the system field when no system message is present", async () => { + const fetchMock = jest + .fn() + .mockResolvedValue(okResponse({ content: [{ text: "" }] })); + globalThis.fetch = fetchMock as typeof fetch; + + await new AnthropicProvider().sendMessage( + [{ id: "1", role: "user", content: "Hi", timestamp: 0 }], + config, + ); + + const sent = JSON.parse(fetchMock.mock.calls[0][1].body); + expect("system" in sent).toBe(false); + }); +}); + +describe("AnthropicProvider.streamMessage", () => { + beforeEach(() => installMockXHR()); + + it("only emits text_delta tokens from content_block_delta events", async () => { + const tokens: string[] = []; + let completed = ""; + const provider = new AnthropicProvider(); + + const promise = provider.streamMessage(messages, config, { + onToken: t => tokens.push(t), + onComplete: r => { + completed = r.message.content; + }, + }); + + const xhr = lastXHR(); + xhr.pushChunk('data: {"type":"message_start"}\n'); + xhr.pushChunk( + 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Hi"}}\n', + ); + xhr.pushChunk( + 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"!"}}\n', + ); + xhr.pushChunk('data: {"type":"message_stop"}\n'); + xhr.finish(200); + + await promise; + + expect(tokens).toEqual(["Hi", "!"]); + expect(completed).toBe("Hi!"); + }); + + it("maps a network failure to an AIError via onError", async () => { + let err: Error | undefined; + const promise = new AnthropicProvider().streamMessage(messages, config, { + onError: e => { + err = e; + }, + }); + lastXHR().networkError(); + await promise; + expect(err).toBeInstanceOf(AIError); + expect((err as AIError).provider).toBe("anthropic"); + }); +}); diff --git a/__tests__/ai/gemini.test.ts b/__tests__/ai/gemini.test.ts new file mode 100644 index 0000000..3e73e0b --- /dev/null +++ b/__tests__/ai/gemini.test.ts @@ -0,0 +1,104 @@ +import { GeminiProvider } from "../../src/services/ai/providers/gemini"; +import { type AIConfig, type AIMessage } from "../../src/services/ai/types"; +import { installMockXHR, lastXHR } from "../helpers/mockXhr"; + +function okResponse(body: unknown) { + return { ok: true, status: 200, json: async () => body } as Response; +} + +const config: AIConfig = { + provider: "gemini", + apiKey: "g-key", + model: "gemini-2.0-flash", +}; + +const messages: AIMessage[] = [ + { id: "s", role: "system", content: "Sys", timestamp: 0 }, + { id: "1", role: "user", content: "Hi", timestamp: 0 }, + { id: "2", role: "assistant", content: "Yo", timestamp: 0 }, +]; + +describe("GeminiProvider.sendMessage", () => { + afterEach(() => jest.restoreAllMocks()); + + it("targets generateContent with the key, mapping roles + system_instruction", async () => { + const fetchMock = jest.fn().mockResolvedValue( + okResponse({ + candidates: [ + { content: { parts: [{ text: "Hello" }] }, finishReason: "STOP" }, + ], + usageMetadata: { + promptTokenCount: 2, + candidatesTokenCount: 1, + totalTokenCount: 3, + }, + }), + ); + globalThis.fetch = fetchMock as typeof fetch; + + const result = await new GeminiProvider().sendMessage(messages, config); + + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toBe( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=g-key", + ); + const sent = JSON.parse(init.body); + expect(sent.system_instruction.parts[0].text).toBe("Sys"); + expect(sent.contents).toEqual([ + { role: "user", parts: [{ text: "Hi" }] }, + { role: "model", parts: [{ text: "Yo" }] }, + ]); + expect(result.message.content).toBe("Hello"); + expect(result.usage?.totalTokens).toBe(3); + expect(result.finishReason).toBe("STOP"); + }); + + it("respects a baseURL override for proxying", async () => { + const fetchMock = jest + .fn() + .mockResolvedValue(okResponse({ candidates: [] })); + globalThis.fetch = fetchMock as typeof fetch; + + await new GeminiProvider().sendMessage(messages, { + ...config, + baseURL: "https://proxy.local/v1beta", + }); + + expect(fetchMock.mock.calls[0][0]).toBe( + "https://proxy.local/v1beta/models/gemini-2.0-flash:generateContent?key=g-key", + ); + }); +}); + +describe("GeminiProvider.streamMessage", () => { + beforeEach(() => installMockXHR()); + + it("uses streamGenerateContent and assembles candidate text", async () => { + const tokens: string[] = []; + let completed = ""; + const provider = new GeminiProvider(); + + const promise = provider.streamMessage(messages, config, { + onToken: t => tokens.push(t), + onComplete: r => { + completed = r.message.content; + }, + }); + + const xhr = lastXHR(); + expect(xhr.url).toContain(":streamGenerateContent?alt=sse&key=g-key"); + + xhr.pushChunk( + 'data: {"candidates":[{"content":{"parts":[{"text":"Hel"}]}}]}\n', + ); + xhr.pushChunk( + 'data: {"candidates":[{"content":{"parts":[{"text":"lo"}]}}]}\n', + ); + xhr.finish(200); + + await promise; + + expect(tokens).toEqual(["Hel", "lo"]); + expect(completed).toBe("Hello"); + }); +}); diff --git a/__tests__/ai/openai.test.ts b/__tests__/ai/openai.test.ts new file mode 100644 index 0000000..6a05664 --- /dev/null +++ b/__tests__/ai/openai.test.ts @@ -0,0 +1,142 @@ +import { OpenAIProvider } from "../../src/services/ai/providers/openai"; +import { AIError, type AIConfig, type AIMessage } from "../../src/services/ai/types"; +import { installMockXHR, lastXHR } from "../helpers/mockXhr"; + +function okResponse(body: unknown) { + return { ok: true, status: 200, json: async () => body } as Response; +} +function errResponse(status: number, body: unknown) { + return { ok: false, status, json: async () => body } as Response; +} + +const config: AIConfig = { + provider: "openai", + apiKey: "sk-test", + model: "gpt-4o-mini", +}; + +const messages: AIMessage[] = [ + { id: "1", role: "user", content: "Hi", timestamp: 0 }, +]; + +describe("OpenAIProvider.sendMessage", () => { + afterEach(() => jest.restoreAllMocks()); + + it("posts to the chat/completions endpoint with auth + shaped body", async () => { + const fetchMock = jest.fn().mockResolvedValue( + okResponse({ + id: "cmpl-1", + choices: [{ message: { content: "Hello!" }, finish_reason: "stop" }], + usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 }, + }), + ); + globalThis.fetch = fetchMock as typeof fetch; + + const result = await new OpenAIProvider().sendMessage(messages, config); + + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toBe("https://api.openai.com/v1/chat/completions"); + expect(init.headers.Authorization).toBe("Bearer sk-test"); + const sent = JSON.parse(init.body); + expect(sent.model).toBe("gpt-4o-mini"); + expect(sent.stream).toBeUndefined(); + expect(sent.messages).toEqual([{ role: "user", content: "Hi" }]); + expect(sent.temperature).toBe(0.7); + expect(sent.max_tokens).toBe(1024); + + expect(result.message.content).toBe("Hello!"); + expect(result.usage?.totalTokens).toBe(5); + expect(result.finishReason).toBe("stop"); + }); + + it("honors baseURL, temperature and maxTokens overrides", async () => { + const fetchMock = jest + .fn() + .mockResolvedValue(okResponse({ choices: [{ message: { content: "" } }] })); + globalThis.fetch = fetchMock as typeof fetch; + + await new OpenAIProvider().sendMessage(messages, { + ...config, + baseURL: "https://proxy.local/v1", + temperature: 0.1, + maxTokens: 42, + }); + + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toBe("https://proxy.local/v1/chat/completions"); + const sent = JSON.parse(init.body); + expect(sent.temperature).toBe(0.1); + expect(sent.max_tokens).toBe(42); + }); + + it("maps an HTTP error to AIError carrying the message + statusCode", async () => { + globalThis.fetch = jest + .fn() + .mockResolvedValue( + errResponse(401, { error: { message: "Invalid API key" } }), + ) as typeof fetch; + + await expect( + new OpenAIProvider().sendMessage(messages, config), + ).rejects.toMatchObject({ + name: "AIError", + provider: "openai", + statusCode: 401, + message: "Invalid API key", + }); + await expect( + new OpenAIProvider().sendMessage(messages, config), + ).rejects.toBeInstanceOf(AIError); + }); +}); + +describe("OpenAIProvider.streamMessage", () => { + beforeEach(() => installMockXHR()); + + it("assembles tokens from SSE deltas and reports onComplete", async () => { + const tokens: string[] = []; + let completed = ""; + const provider = new OpenAIProvider(); + + const promise = provider.streamMessage(messages, config, { + onToken: t => tokens.push(t), + onComplete: r => { + completed = r.message.content; + }, + }); + + const xhr = lastXHR(); + expect(xhr.headers.Authorization).toBe("Bearer sk-test"); + expect(JSON.parse(xhr.body).stream).toBe(true); + + xhr.pushChunk('data: {"choices":[{"delta":{"content":"Hel"}}]}\n'); + // Split a single SSE line across two network chunks. + xhr.pushChunk('data: {"choices":[{"delta":{"content":"lo'); + xhr.pushChunk(' there"}}]}\n'); + xhr.pushChunk("data: [DONE]\n"); + xhr.finish(200); + + await promise; + + expect(tokens).toEqual(["Hel", "lo there"]); + expect(completed).toBe("Hello there"); + }); + + it("routes an HTTP error through onError as an AIError", async () => { + let err: Error | undefined; + const provider = new OpenAIProvider(); + + const promise = provider.streamMessage(messages, config, { + onError: e => { + err = e; + }, + }); + + lastXHR().fail(429, JSON.stringify({ error: { message: "Rate limited" } })); + await promise; + + expect(err).toBeInstanceOf(AIError); + expect((err as AIError).statusCode).toBe(429); + expect(err?.message).toBe("Rate limited"); + }); +}); diff --git a/__tests__/ai/sse.test.ts b/__tests__/ai/sse.test.ts new file mode 100644 index 0000000..4a925ef --- /dev/null +++ b/__tests__/ai/sse.test.ts @@ -0,0 +1,71 @@ +import { SSEParser } from "../../src/services/ai/sse"; + +describe("SSEParser", () => { + it("extracts data payloads from complete lines", () => { + const parser = new SSEParser(); + expect(parser.feed("data: hello\n")).toEqual(["hello"]); + expect(parser.feed("data: world\n")).toEqual(["world"]); + }); + + it("strips exactly one optional leading space after the colon", () => { + const parser = new SSEParser(); + expect(parser.feed("data:nospace\n")).toEqual(["nospace"]); + expect(parser.feed("data: two-spaces\n")).toEqual([" two-spaces"]); + }); + + it("ignores non-data lines (events, comments, blanks)", () => { + const parser = new SSEParser(); + const out = parser.feed( + "event: message\n: keep-alive comment\n\ndata: kept\n", + ); + expect(out).toEqual(["kept"]); + }); + + it("buffers a data line split across two feeds (the dropped-token bug)", () => { + const parser = new SSEParser(); + // First network chunk ends mid-line — nothing should be emitted yet. + expect(parser.feed("data: hel")).toEqual([]); + // Second chunk completes the line — the full token must arrive intact. + expect(parser.feed("lo world\n")).toEqual(["hello world"]); + }); + + it("handles many small fragments accumulating into one payload", () => { + const parser = new SSEParser(); + expect(parser.feed("da")).toEqual([]); + expect(parser.feed("ta: ")).toEqual([]); + expect(parser.feed("a")).toEqual([]); + expect(parser.feed("bc\n")).toEqual(["abc"]); + }); + + it("emits multiple payloads contained in a single chunk", () => { + const parser = new SSEParser(); + expect(parser.feed("data: a\ndata: b\ndata: c\n")).toEqual([ + "a", + "b", + "c", + ]); + }); + + it("tolerates CRLF line endings", () => { + const parser = new SSEParser(); + expect(parser.feed("data: a\r\ndata: b\r\n")).toEqual(["a", "b"]); + }); + + it("flush() emits a trailing line that has no final newline", () => { + const parser = new SSEParser(); + expect(parser.feed("data: trailing")).toEqual([]); + expect(parser.flush()).toEqual(["trailing"]); + }); + + it("flush() returns nothing when the buffer is empty or non-data", () => { + const parser = new SSEParser(); + expect(parser.flush()).toEqual([]); + parser.feed("event: done"); + expect(parser.flush()).toEqual([]); + }); + + it("passes through the [DONE] sentinel as a payload", () => { + const parser = new SSEParser(); + expect(parser.feed("data: [DONE]\n")).toEqual(["[DONE]"]); + }); +}); diff --git a/__tests__/event-emitter.test.ts b/__tests__/event-emitter.test.ts new file mode 100644 index 0000000..b8bc1f2 --- /dev/null +++ b/__tests__/event-emitter.test.ts @@ -0,0 +1,66 @@ +import eventEmitter, { + EventEmitter, +} from "../src/services/event-emitter"; + +describe("EventEmitter", () => { + it("delivers emitted payloads to registered listeners", () => { + const ee = new EventEmitter(); + const received: unknown[] = []; + ee.on("ping", payload => received.push(payload)); + + expect(ee.emit("ping", { n: 1 })).toBe(true); + expect(received).toEqual([{ n: 1 }]); + }); + + it("emit returns false when there are no listeners", () => { + expect(new EventEmitter().emit("nobody")).toBe(false); + }); + + it("off removes a listener", () => { + const ee = new EventEmitter(); + const fn = jest.fn(); + ee.on("e", fn); + ee.off("e", fn); + ee.emit("e"); + expect(fn).not.toHaveBeenCalled(); + expect(ee.listenerCount("e")).toBe(0); + }); + + it("once fires a listener exactly one time", () => { + const ee = new EventEmitter(); + const fn = jest.fn(); + ee.once("e", fn); + ee.emit("e"); + ee.emit("e"); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it("removeAllListeners clears a single event or everything", () => { + const ee = new EventEmitter(); + ee.on("a", jest.fn()); + ee.on("b", jest.fn()); + ee.removeAllListeners("a"); + expect(ee.listenerCount("a")).toBe(0); + expect(ee.listenerCount("b")).toBe(1); + ee.removeAllListeners(); + expect(ee.listenerCount("b")).toBe(0); + }); + + it("a listener that removes itself mid-emit does not break siblings", () => { + const ee = new EventEmitter(); + const order: number[] = []; + const a = () => { + order.push(1); + ee.off("e", a); + }; + const b = () => order.push(2); + ee.on("e", a); + ee.on("e", b); + ee.emit("e"); + expect(order).toEqual([1, 2]); + }); + + it("exports a shared singleton instance", () => { + expect(eventEmitter).toBeInstanceOf(EventEmitter); + }); +}); diff --git a/__tests__/helpers/mockXhr.ts b/__tests__/helpers/mockXhr.ts new file mode 100644 index 0000000..3456ac3 --- /dev/null +++ b/__tests__/helpers/mockXhr.ts @@ -0,0 +1,75 @@ +/** + * Controllable XMLHttpRequest mock for testing the SSE streaming transport. + * Lets a test drive `onprogress` / `onload` with progressive `responseText`. + */ +export class MockXHR { + static instances: MockXHR[] = []; + + status = 0; + responseText = ""; + + onprogress: (() => void) | null = null; + onload: (() => void) | null = null; + onerror: (() => void) | null = null; + ontimeout: (() => void) | null = null; + + method = ""; + url = ""; + headers: Record = {}; + body = ""; + + open(method: string, url: string): void { + this.method = method; + this.url = url; + } + + setRequestHeader(key: string, value: string): void { + this.headers[key] = value; + } + + send(body: string): void { + this.body = body; + MockXHR.instances.push(this); + } + + // ── test driver helpers ─────────────────────────────────────────────────── + + /** Append a chunk and fire onprogress (status stays < 400 for streaming). */ + pushChunk(chunk: string, status = 200): void { + this.status = status; + this.responseText += chunk; + this.onprogress?.(); + } + + /** Append a final chunk (optional) and fire onload to finish the stream. */ + finish(status = 200, finalChunk = ""): void { + this.status = status; + this.responseText += finalChunk; + this.onload?.(); + } + + /** Simulate an HTTP error: body present, then onload sees status >= 400. */ + fail(status: number, body = ""): void { + this.status = status; + this.responseText = body; + this.onload?.(); + } + + /** Simulate a transport-level network failure. */ + networkError(): void { + this.onerror?.(); + } +} + +export function installMockXHR(): typeof MockXHR { + MockXHR.instances = []; + // @ts-expect-error overriding the global for the duration of a test + globalThis.XMLHttpRequest = MockXHR; + return MockXHR; +} + +export function lastXHR(): MockXHR { + const xhr = MockXHR.instances[MockXHR.instances.length - 1]; + if (!xhr) throw new Error("No XMLHttpRequest was created"); + return xhr; +} diff --git a/__tests__/helpers/renderHook.tsx b/__tests__/helpers/renderHook.tsx new file mode 100644 index 0000000..f767a2d --- /dev/null +++ b/__tests__/helpers/renderHook.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import TestRenderer, { act } from "react-test-renderer"; + +export interface RenderHookResult { + result: { current: T }; + rerender: () => void; + unmount: () => void; +} + +/** + * Minimal `renderHook` built on react-test-renderer. The hooks under test do + * not render any React Native host components, so this avoids pulling the full + * native testing stack into pure-logic hook tests. + */ +export function renderHook(useHook: () => T): RenderHookResult { + const result = { current: undefined as unknown as T }; + + function HookHost() { + result.current = useHook(); + return null; + } + + let renderer!: TestRenderer.ReactTestRenderer; + act(() => { + renderer = TestRenderer.create(); + }); + + return { + result, + rerender: () => act(() => renderer.update()), + unmount: () => act(() => renderer.unmount()), + }; +} + +export { act }; diff --git a/__tests__/hooks/useAIChat.test.tsx b/__tests__/hooks/useAIChat.test.tsx new file mode 100644 index 0000000..a54b44a --- /dev/null +++ b/__tests__/hooks/useAIChat.test.tsx @@ -0,0 +1,119 @@ +import { useAIChat } from "../../src/hooks/useAIChat"; +import type { AIConfig } from "../../src/services/ai/types"; +import { installMockXHR, lastXHR } from "../helpers/mockXhr"; +import { act, renderHook } from "../helpers/renderHook"; + +function okResponse(body: unknown) { + return { ok: true, status: 200, json: async () => body } as Response; +} + +const config: AIConfig = { + provider: "openai", + apiKey: "sk", + model: "gpt-4o-mini", +}; + +describe("useAIChat.sendMessage", () => { + afterEach(() => jest.restoreAllMocks()); + + it("appends the user message then the assistant reply", async () => { + globalThis.fetch = jest.fn().mockResolvedValue( + okResponse({ choices: [{ message: { content: "Pong" } }] }), + ) as typeof fetch; + + const { result } = renderHook(() => useAIChat({ config })); + + await act(async () => { + await result.current.sendMessage("Ping"); + }); + + const { messages } = result.current; + expect(messages).toHaveLength(2); + expect(messages[0]).toMatchObject({ role: "user", content: "Ping" }); + expect(messages[1]).toMatchObject({ role: "assistant", content: "Pong" }); + expect(result.current.isLoading).toBe(false); + }); + + it("sets error state when the request fails", async () => { + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 401, + json: async () => ({ error: { message: "bad key" } }), + }) as typeof fetch; + + const { result } = renderHook(() => useAIChat({ config })); + + await act(async () => { + await result.current.sendMessage("Ping"); + }); + + expect(result.current.error?.message).toBe("bad key"); + expect(result.current.isLoading).toBe(false); + }); +}); + +describe("useAIChat.streamMessage", () => { + beforeEach(() => installMockXHR()); + + it("fills the placeholder assistant message token by token", async () => { + const { result } = renderHook(() => useAIChat({ config })); + + let promise!: Promise; + await act(async () => { + promise = result.current.streamMessage("Ping"); + }); + + const xhr = lastXHR(); + await act(async () => { + xhr.pushChunk('data: {"choices":[{"delta":{"content":"Po"}}]}\n'); + xhr.pushChunk('data: {"choices":[{"delta":{"content":"ng"}}]}\n'); + xhr.pushChunk("data: [DONE]\n"); + xhr.finish(200); + await promise; + }); + + const { messages } = result.current; + expect(messages[0]).toMatchObject({ role: "user", content: "Ping" }); + expect(messages[messages.length - 1]).toMatchObject({ + role: "assistant", + content: "Pong", + }); + expect(result.current.isStreaming).toBe(false); + }); +}); + +describe("useAIChat helpers", () => { + it("setSystemPrompt inserts a single system message at the front", async () => { + const { result } = renderHook(() => useAIChat({ config })); + + await act(async () => { + result.current.setSystemPrompt("First"); + result.current.setSystemPrompt("Second"); + }); + + const systemMsgs = result.current.messages.filter(m => m.role === "system"); + expect(systemMsgs).toHaveLength(1); + expect(result.current.messages[0]).toMatchObject({ + role: "system", + content: "Second", + }); + }); + + it("clearMessages empties the conversation and error", async () => { + globalThis.fetch = jest + .fn() + .mockResolvedValue(okResponse({ choices: [{ message: { content: "x" } }] })); + + const { result } = renderHook(() => useAIChat({ config })); + await act(async () => { + await result.current.sendMessage("hi"); + }); + expect(result.current.messages.length).toBeGreaterThan(0); + + await act(async () => { + result.current.clearMessages(); + }); + expect(result.current.messages).toHaveLength(0); + expect(result.current.error).toBeNull(); + }); +}); diff --git a/__tests__/hooks/useAICompletion.test.tsx b/__tests__/hooks/useAICompletion.test.tsx new file mode 100644 index 0000000..24b9b18 --- /dev/null +++ b/__tests__/hooks/useAICompletion.test.tsx @@ -0,0 +1,76 @@ +import { useAICompletion } from "../../src/hooks/useAICompletion"; +import type { AIConfig } from "../../src/services/ai/types"; +import { act, renderHook } from "../helpers/renderHook"; + +function okResponse(body: unknown) { + return { ok: true, status: 200, json: async () => body } as Response; +} + +const config: AIConfig = { + provider: "openai", + apiKey: "sk", + model: "gpt-4o-mini", +}; + +describe("useAICompletion", () => { + afterEach(() => jest.restoreAllMocks()); + + it("returns the completion text and clears loading", async () => { + globalThis.fetch = jest.fn().mockResolvedValue( + okResponse({ choices: [{ message: { content: "42" } }] }), + ) as typeof fetch; + + const { result } = renderHook(() => useAICompletion({ config })); + + let answer: string | null = null; + await act(async () => { + answer = await result.current.complete("what is 6x7?"); + }); + + expect(answer).toBe("42"); + expect(result.current.result).toBe("42"); + expect(result.current.isLoading).toBe(false); + expect(result.current.error).toBeNull(); + }); + + it("prepends the systemPrompt as a system message", async () => { + const fetchMock = jest + .fn() + .mockResolvedValue(okResponse({ choices: [{ message: { content: "ok" } }] })); + globalThis.fetch = fetchMock as typeof fetch; + + const { result } = renderHook(() => + useAICompletion({ config, systemPrompt: "Classify sentiment." }), + ); + + await act(async () => { + await result.current.complete("I love it"); + }); + + const sent = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(sent.messages[0]).toMatchObject({ + role: "system", + content: "Classify sentiment.", + }); + expect(sent.messages[1]).toMatchObject({ role: "user", content: "I love it" }); + }); + + it("captures errors and returns null", async () => { + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 500, + json: async () => ({ error: { message: "server error" } }), + }) as typeof fetch; + + const { result } = renderHook(() => useAICompletion({ config })); + + let answer: string | null = "unset"; + await act(async () => { + answer = await result.current.complete("hi"); + }); + + expect(answer).toBeNull(); + expect(result.current.error).not.toBeNull(); + expect(result.current.error?.message).toBe("server error"); + }); +}); diff --git a/jest.config.js b/jest.config.js index 8eb675e..c9fad40 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,12 @@ module.exports = { preset: 'react-native', + // Only files ending in .test.ts(x) are suites, so shared fixtures can live + // under __tests__/helpers without jest trying to run them as empty suites. + testMatch: ['**/__tests__/**/*.test.{ts,tsx}'], + setupFiles: ['/jest.setup.js'], + // The RN preset only transforms react-native* by default; the app also pulls + // in ESM-published packages that must be transpiled for jest. + transformIgnorePatterns: [ + 'node_modules/(?!(?:jest-)?(?:@react-native|react-native|@react-native-community|@react-navigation|react-native-.*|react-navigation-helpers|@freakycoder/.*)/)', + ], }; diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..95159d4 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,38 @@ +/* eslint-disable no-undef */ +// Jest setup: mock the native modules the app touches so component/integration +// tests can render without a native runtime. Pure logic tests (AI service, SSE, +// hooks) don't need these, but the App smoke test does. + +require('react-native-gesture-handler/jestSetup'); + +jest.mock('react-native-reanimated', () => + require('react-native-reanimated/mock'), +); + +jest.mock('react-native-splash-screen', () => ({ + hide: jest.fn(), + show: jest.fn(), +})); + +jest.mock('react-native-dynamic-vector-icons', () => { + const React = require('react'); + const MockIcon = props => React.createElement('Icon', props, null); + return { + __esModule: true, + default: MockIcon, + IconType: { Ionicons: 'Ionicons' }, + }; +}); + +jest.mock('@freakycoder/react-native-bounceable', () => { + const React = require('react'); + return { + __esModule: true, + default: props => React.createElement('Bounceable', props, props.children), + }; +}); + +jest.mock('@freakycoder/react-native-helpers', () => ({ + isAndroid: false, + isIOS: true, +})); From 60c2d9686a00c00583783985953eafbf2513a76e Mon Sep 17 00:00:00 2001 From: kuraydev Date: Mon, 29 Jun 2026 16:12:27 +0300 Subject: [PATCH 4/6] ci: add typecheck/lint/format/test workflow on Node 22 & 24 --- .github/workflows/ci.yml | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..63ea585 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +# Cancel superseded runs on the same ref. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: Lint · Typecheck · Test (Node ${{ matrix.node }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [22.x, 24.x] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node ${{ matrix.node }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: npm + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Typecheck + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Prettier (check) + run: npm run format:check + + - name: Test + run: npm test -- --runInBand --ci From 1af970d9228e26f336f3c49483efc7b04f2efdd9 Mon Sep 17 00:00:00 2001 From: kuraydev Date: Mon, 29 Jun 2026 16:12:27 +0300 Subject: [PATCH 5/6] chore(deps): drop dead deps, make eslint deps explicit, add metadata Remove unused/dead deps (event, metro-react-native-babel-preset, eslint-plugin-flowtype, eslint-plugin-ft-flow, redundant @trivago import sorter) and the chalk/ora-based runner scripts + duplicate .prettierrc.js. Promote the transitively-relied-on @typescript-eslint parser/plugin and eslint-plugin-jest to direct devDeps. Add typecheck/lint:fix/format:check scripts, package metadata (description/license/author/repo/bugs/keywords), and run typecheck+lint in the husky pre-commit. --- .husky/pre-commit | 4 +- .prettierrc.js | 5 - package-lock.json | 817 ++++++++---------------------- package.json | 42 +- src/scripts/terminal/lint.mjs | 19 - src/scripts/terminal/prettier.mjs | 16 - 6 files changed, 235 insertions(+), 668 deletions(-) delete mode 100644 .prettierrc.js delete mode 100644 src/scripts/terminal/lint.mjs delete mode 100644 src/scripts/terminal/prettier.mjs diff --git a/.husky/pre-commit b/.husky/pre-commit index 8e028d2..f1e20b2 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,2 @@ -node src/scripts/terminal/prettier.mjs -node src/scripts/terminal/lint.mjs +npm run typecheck +npm run lint diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 06860c8..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - arrowParens: 'avoid', - singleQuote: true, - trailingComma: 'all', -}; diff --git a/package-lock.json b/package-lock.json index ce3f897..c77ce18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "ReactNativeTypescriptBoilerplate", "version": "0.0.1", + "license": "MIT", "dependencies": { "@freakycoder/react-native-bounceable": "^2.1.0", "@freakycoder/react-native-custom-text": "^0.1.2", @@ -18,7 +19,6 @@ "@react-navigation/stack": "^7.8.6", "axios": "^1.13.6", "axios-hooks": "^5.1.1", - "event": "^1.0.0", "i18next": "^25.8.20", "react": "19.2.3", "react-i18next": "^16.5.8", @@ -47,17 +47,17 @@ "@react-native/eslint-config": "0.84.1", "@react-native/metro-config": "0.84.1", "@react-native/typescript-config": "0.84.1", - "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/jest": "^29.5.13", "@types/react": "^19.2.0", "@types/react-test-renderer": "^19.1.0", + "@typescript-eslint/eslint-plugin": "^8.57.1", + "@typescript-eslint/parser": "^8.57.1", "babel-plugin-module-resolver": "^5.0.3", - "eslint": "^8.19.0", + "eslint": "^8.57.1", "eslint-config-prettier": "10.1.8", "eslint-plugin-eslint-comments": "3.2.0", - "eslint-plugin-flowtype": "8.0.3", - "eslint-plugin-ft-flow": "3.0.11", "eslint-plugin-import": "2.32.0", + "eslint-plugin-jest": "^29.0.1", "eslint-plugin-prettier": "5.5.5", "eslint-plugin-promise": "7.2.1", "eslint-plugin-react": "7.37.5", @@ -66,7 +66,6 @@ "eslint-plugin-unused-imports": "4.4.1", "husky": "^9.1.7", "jest": "^29.6.3", - "metro-react-native-babel-preset": "^0.77.0", "prettier": "3.8.1", "react-test-renderer": "19.2.3", "typescript": "^5.8.3" @@ -103,7 +102,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -135,7 +133,6 @@ "integrity": "sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", @@ -235,7 +232,7 @@ "version": "0.6.8", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", @@ -248,19 +245,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -338,7 +322,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -413,7 +397,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -536,146 +520,14 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-export-default-from": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz", "integrity": "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -752,7 +604,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -765,7 +617,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.28.6.tgz", "integrity": "sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -781,9 +633,8 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, @@ -1017,7 +868,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1035,7 +886,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -1069,7 +920,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1085,7 +936,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -1119,7 +970,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1157,7 +1008,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1289,7 +1140,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1306,7 +1157,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1474,7 +1325,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -1507,7 +1358,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1576,7 +1427,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1592,7 +1443,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1625,7 +1476,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -1642,7 +1493,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1676,7 +1527,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1692,9 +1543,8 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-module-imports": "^7.28.6", @@ -1713,7 +1563,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1729,7 +1579,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1745,7 +1595,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1794,7 +1644,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -1815,7 +1665,7 @@ "version": "0.13.0", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", @@ -2659,14 +2509,14 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/topo": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" @@ -3313,7 +3163,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -3327,7 +3177,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -3337,7 +3187,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -3364,9 +3214,8 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.1.0.tgz", "integrity": "sha512-441WsVtRe4nGJ9OzA+QMU1+22lA6Q2hRWqqIMKD0wjEMLqcSfOZyu2UL9a/yRpL/dRpyUsU4n7AxqKfTKO/Csg==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@react-native-community/cli-clean": "20.1.0", "@react-native-community/cli-config": "20.1.0", @@ -3395,7 +3244,7 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-20.1.0.tgz", "integrity": "sha512-77L4DifWfxAT8ByHnkypge7GBMYpbJAjBGV+toowt5FQSGaTBDcBHCX+FFqFRukD5fH6i8sZ41Gtw+nbfCTTIA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-tools": "20.1.0", @@ -3408,7 +3257,7 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-20.1.0.tgz", "integrity": "sha512-1x9rhLLR/dKKb92Lb5O0l0EmUG08FHf+ZVyVEf9M+tX+p5QIm52MRiy43R0UAZ2jJnFApxRk+N3sxoYK4Dtnag==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-tools": "20.1.0", @@ -3423,7 +3272,7 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-android/-/cli-config-android-20.1.0.tgz", "integrity": "sha512-3A01ZDyFeCALzzPcwP/fleHoP3sGNq1UX7FzxkTrOFX8RRL9ntXNXQd27E56VU4BBxGAjAJT4Utw8pcOjJceIA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-tools": "20.1.0", @@ -3436,7 +3285,7 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-20.1.0.tgz", "integrity": "sha512-n6JVs8Q3yxRbtZQOy05ofeb1kGtspGN3SgwPmuaqvURF9fsuS7c4/9up2Kp9C+1D2J1remPJXiZLNGOcJvfpOA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-tools": "20.1.0", @@ -3449,7 +3298,7 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-20.1.0.tgz", "integrity": "sha512-QfJF1GVjA4PBrIT3SJ0vFFIu0km1vwOmLDlOYVqfojajZJ+Dnvl0f94GN1il/jT7fITAxom///XH3/URvi7YTQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-config": "20.1.0", @@ -3473,7 +3322,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3486,7 +3335,7 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-20.1.0.tgz", "integrity": "sha512-TeHPDThOwDppQRpndm9kCdRCBI8AMy3HSIQ+iy7VYQXL5BtZ5LfmGdusoj7nVN/ZGn0Lc6Gwts5qowyupXdeKg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-config-android": "20.1.0", @@ -3500,7 +3349,7 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-20.1.0.tgz", "integrity": "sha512-0ih1hrYezSM2cuOlVnwBEFtMwtd8YgpTLmZauDJCv50rIumtkI1cQoOgLoS4tbPCj9U/Vn2a9BFH0DLFOOIacg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-config-apple": "20.1.0", @@ -3514,7 +3363,7 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-20.1.0.tgz", "integrity": "sha512-XN7Da9z4WsJxtqVtEzY8q2bv22OsvzaFP5zy5+phMWNoJlU4lf7IvBSxqGYMpQ9XhYP7arDw5vmW4W34s06rnA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-platform-apple": "20.1.0" @@ -3524,7 +3373,7 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-20.1.0.tgz", "integrity": "sha512-Tb415Oh8syXNT2zOzLzFkBXznzGaqKCiaichxKzGCDKg6JGHp3jSuCmcTcaPeYC7oc32n/S3Psw7798r4Q/7lA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@react-native-community/cli-tools": "20.1.0", @@ -3543,7 +3392,7 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-20.1.0.tgz", "integrity": "sha512-/YmzHGOkY6Bgrv4OaA1L8rFqsBlQd1EB2/ipAoKPiieV0EcB5PUamUSuNeFU3sBZZTYQCUENwX4wgOHgFUlDnQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@vscode/sudo-prompt": "^9.0.0", @@ -3562,7 +3411,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3575,7 +3424,7 @@ "version": "20.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-20.1.0.tgz", "integrity": "sha512-D0kDspcwgbVXyNjwicT7Bb1JgXjijTw1JJd+qxyF/a9+sHv7TU4IchV+gN38QegeXqVyM4Ym7YZIvXMFBmyJqA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "joi": "^17.2.1" @@ -3585,7 +3434,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3599,7 +3448,6 @@ "resolved": "https://registry.npmjs.org/@react-native-masked-view/masked-view/-/masked-view-0.3.2.tgz", "integrity": "sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ==", "license": "MIT", - "peer": true, "peerDependencies": { "react": ">=16", "react-native": ">=0.57" @@ -3618,7 +3466,7 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.84.1.tgz", "integrity": "sha512-vorvcvptGxtK0qTDCFQb+W3CU6oIhzcX5dduetWRBoAhXdthEQM0MQnF+GTXoXL8/luffKgy7PlZRG/WeI/oRQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.3", @@ -3632,7 +3480,7 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.84.1.tgz", "integrity": "sha512-3GpmCKk21f4oe32bKIdmkdn+WydvhhZL+1nsoFBGi30Qrq9vL16giKu31OcnWshYz139x+mVAvCyoyzgn8RXSw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -3925,7 +3773,7 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.84.1.tgz", "integrity": "sha512-NswINguTz0eg1Dc0oGO/1dejXSr6iQaz8/NnCRn5HJdA3dGfqadS7zlYv0YjiWpgKgcW6uENaIEgJOQww0KSpw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -3944,9 +3792,8 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.84.1.tgz", "integrity": "sha512-KlRawK4aXxRLlR3HYVfZKhfQp7sejQefQ/LttUWUkErhKO0AFt+yznoSLq7xwIrH9K3A3YwImHuFVtUtuDmurA==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@react-native/js-polyfills": "0.84.1", "@react-native/metro-babel-transformer": "0.84.1", @@ -4077,7 +3924,6 @@ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.34.tgz", "integrity": "sha512-zzQ0mKAhLsjTIsaoLfILKZVMObJzE0F+bOi0hl2Glt+1Rd2GtaWJ1Z024c3yLmX+Oc79pqoCQLBXpyxtrZu9NQ==", "license": "MIT", - "peer": true, "dependencies": { "@react-navigation/core": "^7.16.2", "escape-string-regexp": "^4.0.0", @@ -4129,7 +3975,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" @@ -4139,14 +3985,14 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@simple-libs/child-process-utils": { @@ -4202,47 +4048,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@trivago/prettier-plugin-sort-imports": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.2.tgz", - "integrity": "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@babel/generator": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", - "javascript-natural-sort": "^0.7.1", - "lodash-es": "^4.17.21", - "minimatch": "^9.0.0", - "parse-imports-exports": "^0.2.4" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@vue/compiler-sfc": "3.x", - "prettier": "2.x - 3.x", - "prettier-plugin-ember-template-tag": ">= 2.0.0", - "prettier-plugin-svelte": "3.x", - "svelte": "4.x || 5.x" - }, - "peerDependenciesMeta": { - "@vue/compiler-sfc": { - "optional": true - }, - "prettier-plugin-ember-template-tag": { - "optional": true - }, - "prettier-plugin-svelte": { - "optional": true - }, - "svelte": { - "optional": true - } - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4346,7 +4151,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -4355,9 +4159,8 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4399,7 +4202,6 @@ "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.1", @@ -4690,7 +4492,7 @@ "version": "9.3.2", "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz", "integrity": "sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/abort-controller": { @@ -4709,7 +4511,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -4723,7 +4525,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4734,7 +4536,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4817,7 +4618,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/ansi-fragments/-/ansi-fragments-0.2.1.tgz", "integrity": "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "colorette": "^1.0.7", @@ -4829,7 +4630,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4839,7 +4640,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^4.1.0" @@ -4889,14 +4690,14 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz", "integrity": "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "devOptional": true, + "dev": true, "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { @@ -5076,7 +4877,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -5096,7 +4897,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/asynckit": { @@ -5126,7 +4927,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -5261,7 +5061,7 @@ "version": "0.4.17", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -5290,7 +5090,7 @@ "version": "0.6.8", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8" @@ -5312,7 +5112,7 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" @@ -5402,7 +5202,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -5414,7 +5214,7 @@ "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -5439,7 +5239,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -5449,7 +5249,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { @@ -5493,7 +5293,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5521,7 +5320,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -5552,7 +5351,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -5594,7 +5393,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5611,7 +5410,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5754,7 +5553,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "restore-cursor": "^3.1.0" @@ -5767,7 +5566,7 @@ "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5794,7 +5593,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.8" @@ -5863,7 +5662,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -5882,14 +5681,14 @@ "version": "1.2.9", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || >=14" @@ -5910,7 +5709,7 @@ "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" @@ -5923,7 +5722,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -5942,7 +5741,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -5952,7 +5751,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -5995,7 +5794,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6054,7 +5853,7 @@ "version": "3.49.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.28.1" @@ -6068,9 +5867,8 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -6150,7 +5948,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -6211,7 +6009,7 @@ "version": "1.11.20", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -6235,7 +6033,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6276,7 +6074,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6286,7 +6084,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "clone": "^1.0.2" @@ -6472,7 +6270,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6482,7 +6280,7 @@ "version": "7.21.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "envinfo": "dist/cli.js" @@ -6495,7 +6293,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -6514,7 +6312,7 @@ "version": "1.5.2", "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.2.tgz", "integrity": "sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -6736,7 +6534,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6793,7 +6590,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6894,40 +6690,6 @@ "node": ">= 4" } }, - "node_modules/eslint-plugin-flowtype": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", - "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "lodash": "^4.17.21", - "string-natural-compare": "^3.0.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@babel/plugin-syntax-flow": "^7.14.5", - "@babel/plugin-transform-react-jsx": "^7.14.9", - "eslint": "^8.1.0" - } - }, - "node_modules/eslint-plugin-ft-flow": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/eslint-plugin-ft-flow/-/eslint-plugin-ft-flow-3.0.11.tgz", - "integrity": "sha512-6ZJ4KYGYjIosCcU883zBBT1nFsKP58xrTOwguiw3/HRq0EpYAyhrF1nCGbK7V23cmKtPXMpDfl8qPupt5s5W8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21", - "string-natural-compare": "^3.0.1" - }, - "peerDependencies": { - "eslint": "^8.56.0 || ^9.0.0", - "hermes-eslint": ">=0.15.0" - } - }, "node_modules/eslint-plugin-import": { "version": "2.32.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", @@ -7453,16 +7215,6 @@ "node": ">= 0.6" } }, - "node_modules/event": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/event/-/event-1.0.0.tgz", - "integrity": "sha512-/q6sbFBp32KzPv3nDAt3eio279pruEGP8JSvDpdHy7dhuQAYW1pvaDMq+vKm8+JS8PO5erorCP71sMpStZErgA==", - "hasInstallScript": true, - "dependencies": { - "method": "~1.0.0", - "reducible": "~1.0.1" - } - }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -7476,7 +7228,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -7545,7 +7297,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -7562,7 +7314,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -7605,7 +7357,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.4.tgz", "integrity": "sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -7624,7 +7376,7 @@ "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -7744,7 +7496,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -7850,7 +7602,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -7999,7 +7751,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -8262,35 +8014,6 @@ "integrity": "sha512-hZ5O7PDz1vQ99TS7HD3FJ9zVynfU1y+VWId6U1Pldvd8hmAYrNec/XLPYJKD3dLOW6NXak6aAQAuMuSo3ji0tQ==", "license": "MIT" }, - "node_modules/hermes-eslint": { - "version": "0.34.0", - "resolved": "https://registry.npmjs.org/hermes-eslint/-/hermes-eslint-0.34.0.tgz", - "integrity": "sha512-w/1UQpIpzzQVNYUrf7nEJ0v8DHjvfpcOeOAquhEYp99VS43l3wUZ5gztYjgWJ7B+r84FqqUA5CG59ZXjd0JDeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esrecurse": "^4.3.0", - "hermes-estree": "0.34.0", - "hermes-parser": "0.34.0" - } - }, - "node_modules/hermes-eslint/node_modules/hermes-parser": { - "version": "0.34.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.34.0.tgz", - "integrity": "sha512-tcgan5UNZvu3WwmR3jDAlmwEAR2CMv8cwQVMe5j0NrLQkstf0l3ULbYPuTZWbXxbPa0PyZPiq5LYEcFVmhM9LQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.34.0" - } - }, - "node_modules/hermes-estree": { - "version": "0.34.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.34.0.tgz", - "integrity": "sha512-6qLylexjmuKa/YYhMiNn/3VejBsdzwmYUGmNpc693/pJzymmbufhkRW/2K6GqFgu0ApRWoqF0NbM6u82jFcOXA==", - "dev": true, - "license": "MIT" - }, "node_modules/hermes-parser": { "version": "0.32.0", "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", @@ -8383,7 +8106,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -8424,7 +8147,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.6" }, @@ -8441,7 +8163,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -8454,7 +8176,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -8500,7 +8222,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -8626,7 +8348,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -8699,7 +8421,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -8765,7 +8487,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8791,7 +8513,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -8831,7 +8553,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -8844,7 +8566,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8987,7 +8709,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9051,7 +8773,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -9110,7 +8832,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -9230,20 +8952,12 @@ "node": ">= 0.4" } }, - "node_modules/javascript-natural-sort": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", - "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", - "dev": true, - "license": "MIT" - }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9929,7 +9643,7 @@ "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.3.0", @@ -9949,7 +9663,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9987,7 +9701,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -10020,7 +9734,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "devOptional": true, + "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -10056,7 +9770,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10066,7 +9780,7 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.1.tgz", "integrity": "sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.1.1", @@ -10125,14 +9839,14 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -10151,13 +9865,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -10169,7 +9876,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/lodash.kebabcase": { @@ -10224,7 +9931,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -10241,7 +9948,7 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ansi-fragments": "^0.2.1", @@ -10256,7 +9963,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -10268,7 +9975,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -10282,7 +9989,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -10295,7 +10002,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -10311,7 +10018,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -10324,7 +10031,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -10339,14 +10046,14 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/logkitty/node_modules/yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "cliui": "^6.0.0", @@ -10369,7 +10076,7 @@ "version": "18.1.3", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "camelcase": "^5.0.0", @@ -10457,7 +10164,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -10492,17 +10199,12 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 8" } }, - "node_modules/method": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/method/-/method-1.0.2.tgz", - "integrity": "sha512-0BxVo2fPZK1gOfbQ7V8MWyiNLB+8dv8rREcOeQHnZdPZgUbdzXUUiqPszm8DY8sTk1ewWyloU6QSMUwF5+jc6Q==" - }, "node_modules/metro": { "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.5.tgz", @@ -10680,71 +10382,6 @@ "node": ">=20.19.4" } }, - "node_modules/metro-react-native-babel-preset": { - "version": "0.77.0", - "resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.77.0.tgz", - "integrity": "sha512-HPPD+bTxADtoE4y/4t1txgTQ1LVR6imOBy7RMHUsqMVTbekoi8Ph5YI9vKX2VMPtVWeFt0w9YnCSLPa76GcXsA==", - "deprecated": "Use @react-native/babel-preset instead", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.20.0", - "@babel/plugin-proposal-async-generator-functions": "^7.0.0", - "@babel/plugin-proposal-class-properties": "^7.18.0", - "@babel/plugin-proposal-export-default-from": "^7.0.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", - "@babel/plugin-proposal-numeric-separator": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.20.0", - "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", - "@babel/plugin-proposal-optional-chaining": "^7.20.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.0", - "@babel/plugin-syntax-export-default-from": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.18.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", - "@babel/plugin-syntax-optional-chaining": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-async-to-generator": "^7.20.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.20.0", - "@babel/plugin-transform-flow-strip-types": "^7.20.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-react-jsx-self": "^7.0.0", - "@babel/plugin-transform-react-jsx-source": "^7.0.0", - "@babel/plugin-transform-runtime": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-sticky-regex": "^7.0.0", - "@babel/plugin-transform-typescript": "^7.5.0", - "@babel/plugin-transform-unicode-regex": "^7.0.0", - "@babel/template": "^7.0.0", - "babel-plugin-transform-flow-enums": "^0.0.2", - "react-refresh": "^0.4.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/metro-react-native-babel-preset/node_modules/react-refresh": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", - "integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/metro-resolver": { "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.5.tgz", @@ -10975,7 +10612,7 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -11018,28 +10655,12 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -11107,7 +10728,7 @@ "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -11117,7 +10738,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -11158,7 +10779,7 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -11181,7 +10802,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -11221,7 +10842,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11346,7 +10967,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -11365,7 +10986,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -11381,7 +11002,7 @@ "version": "6.4.0", "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-wsl": "^1.1.0" @@ -11412,7 +11033,7 @@ "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "bl": "^4.1.0", @@ -11454,7 +11075,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -11470,7 +11091,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -11495,7 +11116,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -11504,21 +11125,11 @@ "node": ">=6" } }, - "node_modules/parse-imports-exports": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", - "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-statements": "1.0.11" - } - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -11533,13 +11144,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-statements": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", - "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", - "dev": true, - "license": "MIT" - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -11580,7 +11184,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -11818,7 +11422,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11887,7 +11490,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "kleur": "^3.0.3", @@ -11951,7 +11554,7 @@ "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -11994,7 +11597,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -12024,7 +11627,7 @@ "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -12041,7 +11644,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -12127,7 +11729,6 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.84.1.tgz", "integrity": "sha512-0PjxOyXRu3tZ8EobabxSukvhKje2HJbsZikR0U+pvS0pYZza2hXKjcSBiBdFN4h9D0S3v6a8kkrDK6WTRKMwzg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.84.1", @@ -12195,7 +11796,6 @@ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz", "integrity": "sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA==", "license": "MIT", - "peer": true, "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", @@ -12248,7 +11848,6 @@ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz", "integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -12259,7 +11858,6 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.24.0.tgz", "integrity": "sha512-SyoiGaDofiyGPFrUkn1oGsAzkRuX1JUvTD9YQQK3G1JGQ5VWkvHgYSsc1K9OrLsDQxN7NmV71O0sHCAh8cBetA==", "license": "MIT", - "peer": true, "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" @@ -12338,7 +11936,6 @@ "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.7.4.tgz", "integrity": "sha512-NYOdM1MwBb3n+AtMqy1tFy3Mn8DliQtd8sbzAVRf9Gc+uvQ0zRfxN7dS8ZzoyX7t6cyQL5THuGhlnX+iFlQTag==", "license": "MIT", - "peer": true, "dependencies": { "@babel/plugin-transform-arrow-functions": "7.27.1", "@babel/plugin-transform-class-properties": "7.27.1", @@ -12515,7 +12112,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -12526,19 +12123,6 @@ "node": ">= 6" } }, - "node_modules/reducible": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reducible/-/reducible-1.0.6.tgz", - "integrity": "sha512-sH74nXS0dz8nQ4qtDpolj9u2nOwoyIg0f8+TQM1bHllTQw95e9hGSBq5Dn0nFTXkJ1VEJ5rTb2UazDhN+KXX9Q==", - "dependencies": { - "method": "~2.0.x" - } - }, - "node_modules/reducible/node_modules/method": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/method/-/method-2.0.0.tgz", - "integrity": "sha512-JqK3SsKDlfW1+6HzlDcpsOBPyEA+gzKUN/3OG3yc6mvbcF/OyKd+OVJRht4TFABOpXV6IQVcc2kt1u5fWNjFCg==" - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -12665,7 +12249,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/reselect": { @@ -12679,7 +12263,7 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -12723,7 +12307,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -12743,7 +12327,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "onetime": "^5.1.0", @@ -12757,7 +12341,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -12827,7 +12411,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -12871,7 +12455,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -12927,7 +12511,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/scheduler": { @@ -13051,7 +12635,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/set-function-length": { @@ -13155,7 +12739,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13175,7 +12759,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13192,7 +12776,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13211,7 +12795,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13252,7 +12836,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/slash": { @@ -13268,7 +12852,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^3.2.0", @@ -13283,7 +12867,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -13296,7 +12880,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -13306,7 +12890,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/source-map": { @@ -13428,7 +13012,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -13602,7 +13186,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13625,7 +13209,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -13650,7 +13234,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13827,7 +13411,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13956,7 +13539,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -14048,9 +13631,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14128,7 +13710,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -14205,7 +13787,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -14236,7 +13818,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -14276,7 +13858,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "defaults": "^1.0.3" @@ -14374,7 +13956,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/which-typed-array": { @@ -14449,7 +14031,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "async-limiter": "~1.0.0" @@ -14516,7 +14098,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -14531,7 +14113,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 1b2f832..1fe3fdd 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,41 @@ "name": "ReactNativeTypescriptBoilerplate", "version": "0.0.1", "private": true, + "description": "Production-grade React Native + TypeScript boilerplate with a provider-agnostic AI service layer (OpenAI, Anthropic, Gemini), theming, navigation, i18n, and AI-assistant guidance files.", + "license": "MIT", + "author": "Kuray (https://github.com/kuraydev)", + "homepage": "https://github.com/kuraydev/react-native-typescript-boilerplate#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/kuraydev/react-native-typescript-boilerplate.git" + }, + "bugs": { + "url": "https://github.com/kuraydev/react-native-typescript-boilerplate/issues" + }, + "keywords": [ + "react-native", + "typescript", + "boilerplate", + "template", + "starter", + "ai", + "openai", + "anthropic", + "gemini", + "streaming", + "navigation", + "i18n" + ], "scripts": { "android": "react-native run-android", "ios": "react-native run-ios", - "lint": "node src/scripts/terminal/lint.mjs", - "prettier": "node src/scripts/terminal/prettier.mjs", "start": "react-native start", "start:fresh": "react-native start --reset-cache", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "prettier": "prettier --write \"src/**/*.{js,ts,tsx}\"", + "format:check": "prettier --check \"src/**/*.{js,ts,tsx}\"", + "typecheck": "tsc --noEmit", "test": "jest", "prepare": "husky", "clean:showcase": "bash scripts/clean-showcase.sh" @@ -24,7 +52,6 @@ "@react-navigation/stack": "^7.8.6", "axios": "^1.13.6", "axios-hooks": "^5.1.1", - "event": "^1.0.0", "i18next": "^25.8.20", "react": "19.2.3", "react-i18next": "^16.5.8", @@ -53,17 +80,17 @@ "@react-native/eslint-config": "0.84.1", "@react-native/metro-config": "0.84.1", "@react-native/typescript-config": "0.84.1", - "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/jest": "^29.5.13", "@types/react": "^19.2.0", "@types/react-test-renderer": "^19.1.0", + "@typescript-eslint/eslint-plugin": "^8.57.1", + "@typescript-eslint/parser": "^8.57.1", "babel-plugin-module-resolver": "^5.0.3", - "eslint": "^8.19.0", + "eslint": "^8.57.1", "eslint-config-prettier": "10.1.8", "eslint-plugin-eslint-comments": "3.2.0", - "eslint-plugin-flowtype": "8.0.3", - "eslint-plugin-ft-flow": "3.0.11", "eslint-plugin-import": "2.32.0", + "eslint-plugin-jest": "^29.0.1", "eslint-plugin-prettier": "5.5.5", "eslint-plugin-promise": "7.2.1", "eslint-plugin-react": "7.37.5", @@ -72,7 +99,6 @@ "eslint-plugin-unused-imports": "4.4.1", "husky": "^9.1.7", "jest": "^29.6.3", - "metro-react-native-babel-preset": "^0.77.0", "prettier": "3.8.1", "react-test-renderer": "19.2.3", "typescript": "^5.8.3" diff --git a/src/scripts/terminal/lint.mjs b/src/scripts/terminal/lint.mjs deleted file mode 100644 index 9abb3e8..0000000 --- a/src/scripts/terminal/lint.mjs +++ /dev/null @@ -1,19 +0,0 @@ -import { exec } from 'child_process'; -import chalk from 'chalk'; -import ora from 'ora'; - -const spinner = ora(chalk.magenta('Linting code...')).start(); -spinner.color = 'magenta'; - -// Adjust the ESLint command and glob pattern as needed for your project. -exec('npx eslint . --fix', (error, stdout, stderr) => { - // Clear the spinner line before outputting the final message. - process.stdout.write('\r\x1b[K'); - - if (error) { - spinner.fail(chalk.bgRed(`ESLint error: ${stderr || stdout}`)); - process.exit(1); - } else { - spinner.succeed(chalk.magentaBright('Code linted successfully!')); - } -}); diff --git a/src/scripts/terminal/prettier.mjs b/src/scripts/terminal/prettier.mjs deleted file mode 100644 index 80f5c1a..0000000 --- a/src/scripts/terminal/prettier.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import { exec } from 'child_process'; -import chalk from 'chalk'; -import ora from 'ora'; - -const spinner = ora(chalk.cyan('Formatting code...')).start(); -spinner.color = 'cyan'; - -// Adjust the Prettier command and glob pattern to match your project. -exec('npx prettier --write "src/**/*.{js,ts,tsx}"', (error, stdout, stderr) => { - if (error) { - spinner.fail(chalk.bgRed(`Prettier error: ${stderr}`)); - process.exit(1); - } else { - spinner.succeed(chalk.cyanBright('Code formatted successfully!')); - } -}); From 4aa67caaef1a00ed765a5b4c1813806cbd35bb3c Mon Sep 17 00:00:00 2001 From: kuraydev Date: Mon, 29 Jun 2026 16:12:34 +0300 Subject: [PATCH 6/6] docs: overhaul README and add project hygiene files Add the missing MIT LICENSE, a Keep-a-Changelog CHANGELOG, CONTRIBUTING, and GitHub issue/PR templates. Fix the README: replace the misleading @freakycoder npm badges with a CI badge + clone-and-go note, add Requirements + New Architecture/Expo notes, a Streaming & Security section documenting the RN fetch-streaming caveat and baseURL proxy guidance, a Troubleshooting section, accurate path-alias notes, and Contributing/ Changelog links. --- .github/ISSUE_TEMPLATE/bug_report.md | 36 ++++++ .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.md | 23 ++++ .github/PULL_REQUEST_TEMPLATE.md | 24 ++++ CHANGELOG.md | 72 ++++++++++++ CONTRIBUTING.md | 67 +++++++++++ LICENSE | 21 ++++ README.md | 136 +++++++++++++++++++--- 8 files changed, 363 insertions(+), 17 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..f185f77 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Report something that isn't working +title: "[Bug] " +labels: bug +--- + +## Describe the bug + +A clear and concise description of what the bug is. + +## Reproduction + +Steps to reproduce the behavior (a minimal repo or snippet is ideal): + +1. +2. +3. + +## Expected behavior + +What you expected to happen. + +## Environment + +- Boilerplate commit / version: +- React Native version: +- React version: +- Node version: +- Platform: iOS / Android (and simulator vs. device) +- Xcode / Android SDK version: + +## Additional context + +Logs, screenshots, or anything else that helps. If this involves the AI service +layer, note the provider and whether streaming was enabled. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0086358 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..b53845b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea or improvement +title: "[Feature] " +labels: enhancement +--- + +## Problem + +What problem are you trying to solve? What's missing or hard today? + +## Proposed solution + +What you'd like to see. If it touches the AI service layer, navigation, or +theming, describe how it fits the existing architecture. + +## Alternatives considered + +Any alternative approaches you've thought about. + +## Additional context + +Links, references, or examples from other projects. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a69a53d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ +# Summary + + + +## Type of change + +- [ ] Bug fix (non-breaking) +- [ ] New feature (non-breaking) +- [ ] Breaking change +- [ ] Docs / tooling / chore + +## Checklist + +- [ ] `npm run typecheck` passes +- [ ] `npm run lint` passes +- [ ] `npm run format:check` passes +- [ ] `npm test` passes +- [ ] I updated `CHANGELOG.md` under `## [Unreleased]` +- [ ] I did not rename public path aliases, hook return shapes, or AI service + exports (or I documented the break above) + +## Related issues + + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6c4527f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,72 @@ +# Changelog + +All notable changes to this project are documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Fixed + +- **AI streaming now works on-device.** All three providers (`openai`, + `anthropic`, `gemini`) used `response.body.getReader()`, which is `undefined` + in React Native's `fetch` — `streamMessage` threw `"No response body"` and no + tokens ever arrived. Streaming now goes through an `XMLHttpRequest`-based SSE + transport (`src/services/ai/sse.ts`) that React Native supports. +- **SSE tokens split across network chunks are no longer dropped.** A new + `SSEParser` buffers partial lines across reads, so a `data:` line that arrives + in two pieces is reassembled instead of silently lost. +- **`@event-emitter` no longer imports the missing `events` module.** It was + importing Node's `events`, which Metro does not polyfill (and the package was + not installed), so the documented singleton failed to import. Replaced with a + small, dependency-free `EventEmitter` with the same public API. +- **`AIConfig.systemPrompt` is now honored.** It was documented but ignored; + `sendAIMessage` / `streamAIMessage` now prepend it as a system message when no + system message is already present. +- **TypeScript path aliases resolve.** `@models` and `@event-emitter` were used + in code/docs but missing from `tsconfig.json`; `@api` and `@local-storage` + pointed at non-existent folders. Added the missing `tsconfig` paths and real + stub folders, and fixed the `tsconfig`/RN base `moduleResolution` conflict so + `tsc --noEmit` runs clean. +- Static asset (`*.png`) type declarations moved to `assets.d.ts` so they remain + global pattern-ambient modules and `@assets/logo.png` type-resolves. + +### Added + +- **Test suite** (`__tests__/`): unit tests for the SSE parser, all three + providers (request shaping, response parsing, error mapping, streaming), the + service layer + system-prompt injection, the `useAIChat` / `useAICompletion` + hooks, and the event emitter — plus the existing App smoke test now runs under + a proper jest setup. +- **Continuous Integration** (`.github/workflows/ci.yml`): typecheck, lint, + Prettier check, and tests on a Node 22/24 matrix for every push and PR. +- `typecheck`, `lint:fix`, and `format:check` npm scripts; a `LICENSE` file + (MIT); `CONTRIBUTING.md`; a `CHANGELOG.md`; and GitHub issue/PR templates. +- Typed response shapes for the OpenAI / Anthropic / Gemini wire formats, and + `toAIError` / `extractApiErrorMessage` helpers exported from `@services/ai`. +- Gemini's `streamMessage` / `sendMessage` now respect `config.baseURL`, + matching the documented proxy/local-LLM override behavior of the other + providers. + +### Changed + +- **ESLint dependencies made explicit.** `@typescript-eslint/parser`, + `@typescript-eslint/eslint-plugin`, and `eslint-plugin-jest` were required by + the config but only present transitively; they are now direct devDependencies. +- `package.json` metadata: added `description`, `license`, `author`, + `repository`, `bugs`, `homepage`, and `keywords`. +- `npm run lint` / `npm run prettier` now invoke ESLint / Prettier directly + instead of the chalk/ora wrapper scripts (which depended on undeclared deps). +- Husky `pre-commit` runs `typecheck` + `lint`. +- Scoped `LogBox.ignoreAllLogs()` down to an explicit (empty) ignore list so + real warnings surface during development. +- Removed `route: any` and `useEffect((): any => …)` smells in the navigator. + +### Removed + +- Dead dependencies: `event`, `metro-react-native-babel-preset`, + `eslint-plugin-flowtype`, `eslint-plugin-ft-flow`, and the redundant + `@trivago/prettier-plugin-sort-imports` (kept `@ianvs`). +- Duplicate `.prettierrc.js` (the JSON `.prettierrc` is the single source of + truth) and the chalk/ora-based `src/scripts/terminal/*.mjs` runners. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0b20ace --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contributing + +Thanks for your interest in improving this boilerplate! This repo is a +clone-and-go starter template, so contributions that keep it lean, correct, and +well-documented are especially valuable. + +## Getting set up + +```sh +git clone https://github.com/kuraydev/react-native-typescript-boilerplate.git +cd react-native-typescript-boilerplate +npm install +``` + +iOS only: + +```sh +cd ios && pod install && cd .. +``` + +## Before you open a PR + +Run the same gates CI runs — all four must pass: + +```sh +npm run typecheck # tsc --noEmit +npm run lint # eslint . +npm run format:check # prettier --check +npm test # jest +``` + +`npm run lint:fix` and `npm run prettier` auto-fix lint/format issues. + +A Husky `pre-commit` hook runs `typecheck` + `lint` automatically, and +`commit-msg` validates your commit message. + +## Commit messages + +Commits must follow [Conventional Commits](https://www.conventionalcommits.org/) +(enforced by `commitlint`). Allowed types: `feat`, `fix`, `chore`, `docs`, +`style`, `refactor`, `perf`, `test`, `ci`, `revert`. + +``` +feat: add Mistral provider to the AI service layer +fix: buffer SSE lines split across network chunks +docs: document the New Architecture requirement +``` + +## Guidelines + +- **TypeScript:** keep `strict` happy. Prefer typed wire shapes over `as any`. +- **Path aliases:** prefer aliases (`@services/ai`, `@hooks`, `@theme`, …) over + deep relative imports. Add a new alias to **both** `babel.config.js` and + `tsconfig.json`, then restart Metro with `npm run start:fresh`. +- **AI providers:** implement `IAIProvider` and keep the `onToken` / `onComplete` + / `onError` callback contract stable. Reuse `streamSSE` from + `@services/ai` for streaming so line-buffering stays correct. +- **Tests:** add/adjust tests under `__tests__/` for any behavior change. Mock + `fetch` for non-streaming paths and the provided `MockXHR` helper for streaming. +- **Public surface:** this template's de-facto API is its path aliases, hook + return shapes, and AI service exports. Avoid renaming them without a clear, + documented reason. + +## Reporting bugs / requesting features + +Use the issue templates under `.github/ISSUE_TEMPLATE`. For bugs, include your +RN/Node/Xcode versions and a minimal reproduction. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6d60972 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Kuray (kuraydev) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 605e40e..b750f58 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ [![React Native Typescript Boilerplate](https://img.shields.io/badge/-React%20Native%20TypeScript%20Boilerplate-4A6CF7?style=for-the-badge)](https://github.com/kuraydev/react-native-typescript-boilerplate) -[![npm version](https://img.shields.io/npm/v/react-native-typescript-boilerplate.svg?style=for-the-badge)](https://www.npmjs.com/package/@freakycoder/react-native-typescript-boilerplate) -[![npm](https://img.shields.io/npm/dt/react-native-typescript-boilerplate.svg?style=for-the-badge)](https://www.npmjs.com/package/@freakycoder/react-native-typescript-boilerplate) +[![CI](https://img.shields.io/github/actions/workflow/status/kuraydev/react-native-typescript-boilerplate/ci.yml?branch=main&style=for-the-badge&label=CI)](https://github.com/kuraydev/react-native-typescript-boilerplate/actions/workflows/ci.yml) ![Platform - Android and iOS](https://img.shields.io/badge/platform-Android%20%7C%20iOS-blue.svg?style=for-the-badge) -[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](https://opensource.org/licenses/MIT) +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](./LICENSE) [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg?style=for-the-badge)](https://github.com/prettier/prettier) [![AI Ready](https://img.shields.io/badge/AI--Ready-OpenAI%20%7C%20Anthropic%20%7C%20Gemini-8b5cf6?style=for-the-badge)](https://github.com/kuraydev/react-native-typescript-boilerplate) +> **This is a clone-and-go app template, not an npm package** — clone it, rename it, and build your app. (The unrelated `react-native-typescript-boilerplate` npm package is a separate, older project.) + > **AI-Ready.** Production-grade React Native + TypeScript boilerplate with a built-in, provider-agnostic AI service layer. Wire up OpenAI, Anthropic Claude, Google Gemini — or any LLM — in minutes, not days. > [!TIP] @@ -28,9 +29,11 @@ - [Showcase](#-showcase) - [What's New in v6](#-whats-new-in-v6) - [What's Included](#-whats-included) +- [Requirements](#-requirements) - [Getting Started](#-getting-started) - [Path Aliases](#-path-aliases) - [AI Service Layer](#-ai-service-layer) +- [Streaming & Security Notes](#-streaming--security-notes) - [Theme System](#-theme-system) - [Navigation](#-navigation) - [Event Emitter](#-event-emitter) @@ -39,6 +42,8 @@ - [Utilities](#-utilities) - [AI Guidance Files](#-ai-guidance-files) - [Code Quality](#-code-quality) +- [Troubleshooting](#-troubleshooting) +- [Contributing](#-contributing) - [Project Structure](#-project-structure) --- @@ -119,6 +124,33 @@ --- +## 📋 Requirements + +| Tool | Version | Notes | +|---|---|---| +| **Node** | `>= 22.11.0` | enforced via `engines` in `package.json` | +| **React Native** | `0.84.x` | pinned; ships with the **New Architecture** enabled | +| **React** | `19.x` | | +| **Xcode** | `16+` | for iOS builds (CocoaPods `1.16+`) | +| **Android SDK** | API 35 / Build-Tools 35 | for Android builds | +| **Ruby + Bundler** | as per `Gemfile` | for CocoaPods on iOS | + +> [!IMPORTANT] +> **The New Architecture is enabled by default** (`newArchEnabled=true` on +> Android, `RCTNewArchEnabled` on iOS — Bridgeless / JSI / Fabric). This is a +> consumer app template (no custom TurboModules/Fabric components), and every +> native dependency here (`react-native-reanimated` v4 + `react-native-worklets`, +> `react-native-screens` v4, `react-native-gesture-handler`, +> `react-native-safe-area-context`, `@react-native-masked-view/masked-view`, +> `react-native-vector-icons`) is New-Arch compatible at the pinned versions. +> Treat the native pin set as a unit; bumping one can require bumping others. + +> **Expo:** this is a bare React Native project (it has native `ios/` and +> `android/` folders), so it is **not** an Expo-managed app. You can adopt it +> under Expo's bare/prebuild workflow, but that is not the default path here. + +--- + ## 🚀 Getting Started ### 1. Clone @@ -227,8 +259,12 @@ All aliases are defined in `babel.config.js` and `tsconfig.json`. Always prefer | `@utils` | `src/utils` | | `@assets` | `src/assets` | | `@event-emitter` | `src/services/event-emitter` | -| `@api` | `src/services/api/index` *(stub)* | -| `@local-storage` | `src/services/local-storage` *(stub)* | +| `@api` | `src/services/api/index` *(empty placeholder — drop your API client here)* | +| `@local-storage` | `src/services/local-storage` *(empty placeholder — wire up your storage here)* | + +> Every alias above resolves in **both** `babel.config.js` (runtime) and +> `tsconfig.json` (types). `@api` and `@local-storage` are intentional empty +> stubs so the imports type-check out of the box — fill them in as you build. **Example:** @@ -378,6 +414,43 @@ export const AI_PROVIDER_LABELS: Record = { --- +## 🔌 Streaming & Security Notes + +### How streaming works in React Native + +React Native's `fetch` does **not** expose a streaming `ReadableStream` body +(`response.body` is `undefined` on a device/simulator), so the common +`response.body.getReader()` pattern silently fails. This boilerplate streams via +an `XMLHttpRequest`-based Server-Sent Events transport +([`src/services/ai/sse.ts`](./src/services/ai/sse.ts)) that React Native fully +supports, with a cross-chunk line buffer so tokens split across network packets +are never dropped. You don't need to do anything — `streamMessage` / +`streamAIMessage` just work on-device. + +> If you build your own provider, reuse `streamSSE` from `@services/ai` instead +> of `fetch().body` so streaming keeps working on a real device. + +### 🔐 Don't ship API keys in production + +The demo passes a user-entered `apiKey` directly into a client-side request. +That's fine for a local demo, but **never bundle a provider key in a shipped +app** — anyone can extract it from the binary. For production, route requests +through your own backend and point the client at it via `baseURL`: + +```typescript +const config: AIConfig = { + provider: "openai", + apiKey: "", // your proxy injects the real key + model: "gpt-4o-mini", + baseURL: "https://your-api.example.com/ai", // OpenAI-compatible proxy +}; +``` + +The same `baseURL` override also works for local LLMs (e.g. Ollama / LM Studio +exposing an OpenAI-compatible endpoint). + +--- + ## 🎨 Theme System The theme automatically follows the system dark/light mode via `useColorScheme`. @@ -581,27 +654,33 @@ These files are designed to be **extended**. As your project grows, update them ## ✅ Code Quality -### ESLint + Prettier +### Scripts ```sh -# Lint check -npm run lint - -# Auto-format all files in src/ -npm run prettier +npm run typecheck # tsc --noEmit — the type gate +npm run lint # eslint . +npm run lint:fix # eslint . --fix +npm run prettier # prettier --write src/** +npm run format:check # prettier --check src/** (used by CI) +npm test # jest ``` -Config files: `.eslintrc.js`, `.prettierrc`, `.eslintignore`, `.prettierignore` +These are exactly the gates [CI](./.github/workflows/ci.yml) runs on every push +and PR (Node 22 & 24). -### Husky v9 — pre-commit hooks +Config files: `.eslintrc.js`, `.prettierrc`, `.eslintignore`, `.prettierignore`, +`tsconfig.json`, `jest.config.js`. -Husky runs automatically on every commit. No manual setup needed — `npm install` triggers `prepare` which initializes the hooks. +### Husky v9 — git hooks -**On every `git commit`:** -1. `npm run prettier` — auto-formats all staged source files +Husky runs automatically on every commit. No manual setup needed — `npm install` +triggers `prepare` which initializes the hooks. + +**On every `git commit` (`pre-commit`):** +1. `npm run typecheck` — `tsc --noEmit` must pass 2. `npm run lint` — ESLint must pass with no errors -**On the commit message:** +**On the commit message (`commit-msg`):** 3. `commitlint` — validates the message follows Conventional Commits ### Commit conventions (`commitlint`) @@ -624,6 +703,29 @@ Config file: `.commitlintrc.json` --- +## 🧯 Troubleshooting + +| Symptom | Fix | +|---|---| +| `Unable to resolve module @…/…` after adding an alias | Add it to **both** `babel.config.js` and `tsconfig.json`, then `npm run start:fresh` to reset Metro's cache. | +| AI streaming returns nothing / errors immediately | Make sure you're on this version — streaming uses an XHR SSE transport (see [Streaming Notes](#-streaming--security-notes)). `fetch().body.getReader()` does not stream in React Native. | +| `401` / `403` from a provider | Check the API key and that the `model` string is valid for that key. Errors surface as `AIError` with a `statusCode`. | +| iOS build can't find pods | `cd ios && pod install && cd ..` (CocoaPods `1.16+`). | +| Type errors only in CI | Run `npm run typecheck` locally — it's the same `tsc --noEmit` gate. | +| New Architecture build issues | Don't bump a single native dep in isolation; the pinned `reanimated` / `worklets` / `screens` / `gesture-handler` set is validated together. | + +--- + +## 🤝 Contributing + +Contributions are welcome! Please read [`CONTRIBUTING.md`](./CONTRIBUTING.md) for +setup, the local gates (`typecheck` · `lint` · `format:check` · `test`), and the +Conventional Commits convention. All changes go through the +[CI workflow](./.github/workflows/ci.yml). See the +[`CHANGELOG.md`](./CHANGELOG.md) for a history of notable changes. + +--- + ## 📚 Additional Documentation - [Components](./docs/components.md)