diff --git a/eslint.config.mjs b/eslint.config.mjs index 8302e91..397e30d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,7 +25,7 @@ const config = [ { files: ["**/*.{ts,tsx,mts,cts}"], rules: { - "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-unused-expressions": "off", "react-hooks/set-state-in-effect": "off" } diff --git a/next.config.ts b/next.config.ts index 17f7f82..a083a22 100644 --- a/next.config.ts +++ b/next.config.ts @@ -12,30 +12,39 @@ const parseAllowedOrigins = (value: string | undefined): Array => : [] const allowedDevOrigins = parseAllowedOrigins( - process.env.SPAWNDOCK_ALLOWED_DEV_ORIGINS + process.env["SPAWNDOCK_ALLOWED_DEV_ORIGINS"] ) -const previewPath = process.env.SPAWNDOCK_PREVIEW_PATH +const previewPath = process.env["SPAWNDOCK_PREVIEW_PATH"] const serverActionOrigins = parseAllowedOrigins( - process.env.SPAWNDOCK_SERVER_ACTIONS_ALLOWED_ORIGINS + process.env["SPAWNDOCK_SERVER_ACTIONS_ALLOWED_ORIGINS"] ) const normalizedPreviewPath = previewPath && previewPath.length > 0 ? previewPath : undefined -const githubPagesExport = process.env.SPAWNDOCK_GITHUB_PAGES_EXPORT === "1" - -const nextConfig: NextConfig = { - allowedDevOrigins, - assetPrefix: normalizedPreviewPath, - basePath: normalizedPreviewPath, - images: githubPagesExport ? { unoptimized: true } : undefined, - output: githubPagesExport ? "export" : undefined, - trailingSlash: githubPagesExport, - experimental: githubPagesExport - ? undefined - : { +const githubPagesExport = process.env["SPAWNDOCK_GITHUB_PAGES_EXPORT"] === "1" +const previewConfig = normalizedPreviewPath + ? { + assetPrefix: normalizedPreviewPath, + basePath: normalizedPreviewPath + } + : {} +const exportConfig = githubPagesExport + ? { + images: { unoptimized: true }, + output: "export" as const + } + : { + experimental: { serverActions: { allowedOrigins: serverActionOrigins } } + } + +const nextConfig: NextConfig = { + allowedDevOrigins, + trailingSlash: githubPagesExport, + ...previewConfig, + ...exportConfig } export default withNextIntl(nextConfig) diff --git a/package.json b/package.json index 8c840a0..f7e2164 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "ws": "^8.19.0" }, "devDependencies": { + "@effect/language-service": "^0.84.1", "@prover-coder-ai/component-tagger": "^1.0.31", "@prover-coder-ai/eslint-plugin-suggest-members": "^0.0.26", "@spawn-dock/dev-tunnel": "1.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2dea2e..637077d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ importers: specifier: ^8.19.0 version: 8.19.0 devDependencies: + '@effect/language-service': + specifier: ^0.84.1 + version: 0.84.1 '@prover-coder-ai/component-tagger': specifier: ^1.0.31 version: 1.0.31(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(vite@8.0.3(@types/node@24.10.1)(jiti@2.6.1)) @@ -231,6 +234,10 @@ packages: lmdb: optional: true + '@effect/language-service@0.84.1': + resolution: {integrity: sha512-YUqjJU24HeYgPV453cR2fDqkZ+zZKMuxGnmxWAPscWJ6gt6FB7JZohMCOczRTIOGPrQMcloJX7BjCaPu+RNhpw==} + hasBin: true + '@effect/platform-node-shared@0.59.0': resolution: {integrity: sha512-3bq2YKKfLY7UFauZSxqZUneCXoA3SMSls82V+0RKunvRlfPuPQW0hVn6t1RkvEdh0PDoygWG2mZXYQa6Iqgp9A==} peerDependencies: @@ -3882,6 +3889,8 @@ snapshots: effect: 3.21.0 uuid: 11.1.0 + '@effect/language-service@0.84.1': {} + '@effect/platform-node-shared@0.59.0(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': dependencies: '@effect/cluster': 0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) diff --git a/spawndock/publish.mjs b/spawndock/publish.mjs index 0620413..701fb7b 100644 --- a/spawndock/publish.mjs +++ b/spawndock/publish.mjs @@ -1,4 +1,4 @@ -import { execFileSync, spawnSync } from "node:child_process" +import { spawnSync } from "node:child_process" import { cpSync, existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join, resolve } from "node:path" diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index a9dfed1..c46c24a 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -14,16 +14,15 @@ interface ErrorBoundaryState { } export class ErrorBoundary extends Component { - state: ErrorBoundaryState = {}; + override state: ErrorBoundaryState = {}; - // eslint-disable-next-line max-len static getDerivedStateFromError: GetDerivedStateFromError = (error) => ({ error }); - componentDidCatch(error: Error) { + override componentDidCatch(error: Error) { this.setState({ error }); } - render() { + override render() { const { state: { error, diff --git a/src/components/LocaleSwitcher/LocaleSwitcher.tsx b/src/components/LocaleSwitcher/LocaleSwitcher.tsx index f695d5e..bbaed30 100644 --- a/src/components/LocaleSwitcher/LocaleSwitcher.tsx +++ b/src/components/LocaleSwitcher/LocaleSwitcher.tsx @@ -2,11 +2,11 @@ import { Select } from '@telegram-apps/telegram-ui'; import { useLocale } from 'next-intl'; -import { FC } from 'react'; +import type { FC } from 'react'; import { localesMap } from '@/core/i18n/config'; import { setLocale } from '@/core/i18n/locale'; -import { Locale } from '@/core/i18n/types'; +import type { Locale } from '@/core/i18n/types'; export const LocaleSwitcher: FC = () => { const locale = useLocale(); diff --git a/src/components/Page.tsx b/src/components/Page.tsx index 1f69f80..d755320 100644 --- a/src/components/Page.tsx +++ b/src/components/Page.tsx @@ -1,8 +1,8 @@ 'use client'; import { backButton } from '@tma.js/sdk-react'; -import { PropsWithChildren, useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import { useEffect, type PropsWithChildren } from 'react'; export function Page({ children, back = true }: PropsWithChildren<{ /** diff --git a/src/core/init.ts b/src/core/init.ts index 1538625..8cd8e55 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -6,12 +6,14 @@ import { miniApp, viewport, mockTelegramEnv, - type ThemeParams, + type EventPayload, themeParams, retrieveLaunchParams, emitEvent, } from '@tma.js/sdk-react'; +type ThemeEventParams = EventPayload<'theme_changed'>['theme_params']; + /** * Initializes the application and configures its dependencies. */ @@ -39,16 +41,15 @@ export async function init(options: { mockTelegramEnv({ onEvent(event, next) { if (event.name === 'web_app_request_theme') { - let tp: Partial = {}; + let tp: ThemeEventParams = {}; if (firstThemeSent) { - const state = themeParams.state; - tp = state as Partial; + tp = themeParams.state(); } else { firstThemeSent = true; const lp = retrieveLaunchParams(); - tp = (lp.tgWebAppThemeParams || {}) as Partial; + tp = (lp.tgWebAppThemeParams || {}) as ThemeEventParams; } - return emitEvent('theme_changed', { theme_params: tp as any }); + return emitEvent('theme_changed', { theme_params: tp }); } if (event.name === 'web_app_request_safe_area') { @@ -72,7 +73,7 @@ export async function init(options: { try { miniApp.mount(); themeParams.bindCssVars(); - } catch (e) { + } catch { // miniApp not available } @@ -80,7 +81,7 @@ export async function init(options: { viewport.mount().then(() => { viewport.bindCssVars(); }); - } catch (e) { + } catch { // viewport not available } } diff --git a/src/css/bem.ts b/src/css/bem.ts index 989b758..4d655f1 100644 --- a/src/css/bem.ts +++ b/src/css/bem.ts @@ -1,11 +1,11 @@ import { classNames, isRecord } from './classnames'; export interface BlockFn { - (...mods: any): string; + (...mods: readonly unknown[]): string; } export interface ElemFn { - (elem: string, ...mods: any): string; + (elem: string, ...mods: readonly unknown[]): string; } /** @@ -13,7 +13,7 @@ export interface ElemFn { * @param element - element name. * @param mod - mod to apply. */ -function applyMods(element: string, mod: any): string { +function applyMods(element: string, mod: unknown): string { if (Array.isArray(mod)) { return classNames(mod.map((m) => applyMods(element, m))); } @@ -31,7 +31,7 @@ function applyMods(element: string, mod: any): string { * @param element - element name. * @param mods - mod to apply. */ -function computeClassnames(element: string, ...mods: any): string { +function computeClassnames(element: string, ...mods: readonly unknown[]): string { return classNames(element, applyMods(element, mods)); } diff --git a/src/css/classnames.ts b/src/css/classnames.ts index 8625ca6..b64189d 100644 --- a/src/css/classnames.ts +++ b/src/css/classnames.ts @@ -13,7 +13,7 @@ export function isRecord(v: unknown): v is Record { * @param values - values array. * @returns Final class name. */ -export function classNames(...values: any[]): string { +export function classNames(...values: readonly unknown[]): string { return values .map((value) => { if (typeof value === 'string') { @@ -29,6 +29,8 @@ export function classNames(...values: any[]): string { if (Array.isArray(value)) { return classNames(...value); } + + return undefined; }) .filter(Boolean) .join(' '); @@ -46,11 +48,11 @@ type UnionRequiredKeys = U extends U type UnionOptionalKeys = Exclude, UnionRequiredKeys>; -export type MergeClassNames = +export type MergeClassNames = // Removes all types from union that will be ignored by the mergeClassNames function. Exclude< Tuple[number], - number | string | null | undefined | any[] | boolean + number | string | null | undefined | readonly unknown[] | boolean > extends infer Union ? { [K in UnionRequiredKeys]: string } & { [K in UnionOptionalKeys]?: string; @@ -65,15 +67,17 @@ export type MergeClassNames = * @returns An object with keys from all objects with merged values. * @see classNames */ -export function mergeClassNames( +export function mergeClassNames( ...partials: T ): MergeClassNames { return partials.reduce>((acc, partial) => { if (isRecord(partial)) { + const accRecord = acc as Record; + Object.entries(partial).forEach(([key, value]) => { - const className = classNames((acc as any)[key], value); + const className = classNames(accRecord[key], value); if (className) { - (acc as any)[key] = className; + accRecord[key] = className; } }); } diff --git a/src/instrumentation-client.ts b/src/instrumentation-client.ts index 03e0ed3..84ccaed 100644 --- a/src/instrumentation-client.ts +++ b/src/instrumentation-client.ts @@ -11,7 +11,7 @@ mockEnv().then(() => { const { tgWebAppPlatform: platform } = launchParams; const debug = (launchParams.tgWebAppStartParam || '').includes('debug') || - process.env.NODE_ENV === 'development'; + process.env['NODE_ENV'] === 'development'; // Configure all application dependencies. init({ diff --git a/src/mockEnv.ts b/src/mockEnv.ts index 9c9ed02..e540157 100644 --- a/src/mockEnv.ts +++ b/src/mockEnv.ts @@ -1,9 +1,11 @@ -import { mockTelegramEnv, isTMA, emitEvent } from '@tma.js/sdk-react'; +import { mockTelegramEnv, isTMA, emitEvent, type EventPayload } from '@tma.js/sdk-react'; + +type ThemeEventParams = EventPayload<'theme_changed'>['theme_params']; // It is important, to mock the environment only for development purposes. When building the // application, the code inside will be tree-shaken, so you will not see it in your final bundle. export async function mockEnv(): Promise { - return process.env.NODE_ENV !== 'development' + return process.env['NODE_ENV'] !== 'development' ? undefined : isTMA('complete').then((isTma) => { if (!isTma){ @@ -21,14 +23,14 @@ export async function mockEnv(): Promise { section_header_text_color: '#6ab3f3', subtitle_text_color: '#708499', text_color: '#f5f5f5', - } as const; + } as const satisfies ThemeEventParams; const noInsets = { left: 0, top: 0, bottom: 0, right: 0 } as const; mockTelegramEnv({ onEvent(e, next) { // Here you can write your own handlers for all known Telegram Mini Apps methods. if (e.name === 'web_app_request_theme') { - return emitEvent('theme_changed', { theme_params: themeParams as any }); + return emitEvent('theme_changed', { theme_params: themeParams }); } if (e.name === 'web_app_request_viewport') { return emitEvent('viewport_changed', { diff --git a/tsconfig.json b/tsconfig.json index 16a9381..18f97f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,23 @@ { "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "ES2022"], "allowJs": true, + "alwaysStrict": true, + "allowSyntheticDefaultImports": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, "skipLibCheck": true, "strict": true, "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", @@ -16,9 +25,13 @@ "isolatedModules": true, "jsx": "react-jsx", "incremental": true, + "verbatimModuleSyntax": true, "plugins": [ { "name": "next" + }, + { + "name": "@effect/language-service" } ], "paths": { @@ -29,7 +42,7 @@ "./public/*" ] }, - "target": "ES2017" + "target": "ES2022" }, "include": [ "next-env.d.ts",