From 7d1e680a74024f4047438bce8d7a6af678e522bb Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 18 Jul 2026 11:52:25 +0300 Subject: [PATCH] Optimize bundle (#3228) --- app/components/layout/index.tsx | 24 ++++- ...ig-helpers.ts => config-helpers.server.ts} | 0 app/config.server.ts | 2 +- app/config.ts | 97 ++++++++++++++----- app/entry.client.tsx | 94 ++++++++++++------ app/entry.server.tsx | 5 + app/features/build-analyzer/analyzer-hooks.ts | 2 +- .../build-analyzer/core/serializer.ts | 15 +++ app/features/build-analyzer/core/utils.ts | 7 -- .../sendouq/core/SendouQ.server.test.ts | 4 + .../routes/tier-list-maker.tsx | 3 +- app/modules/i18n/useChangeLanguage.ts | 10 +- app/utils/dates.ts | 87 +++++++++++------ app/utils/urls.ts | 2 +- package.json | 1 - pnpm-lock.yaml | 28 ------ types/intl-duration-format.d.ts | 23 ----- vite.config.ts | 9 ++ 18 files changed, 260 insertions(+), 153 deletions(-) rename app/{config-helpers.ts => config-helpers.server.ts} (100%) create mode 100644 app/features/build-analyzer/core/serializer.ts delete mode 100644 types/intl-duration-format.d.ts diff --git a/app/components/layout/index.tsx b/app/components/layout/index.tsx index b8dee259b..7d893adac 100644 --- a/app/components/layout/index.tsx +++ b/app/components/layout/index.tsx @@ -20,7 +20,13 @@ import { } from "react-aria-components"; import { Flipped, Flipper } from "react-flip-toolkit"; import { useTranslation } from "react-i18next"; -import { Link, useFetcher, useLocation, useMatches } from "react-router"; +import { + Link, + useFetcher, + useLocation, + useMatches, + useSearchParams, +} from "react-router"; import { Config } from "~/config"; import { useUser } from "~/features/auth/core/user"; import { useChatContext } from "~/features/chat/useChatContext"; @@ -50,7 +56,6 @@ import { NotificationDot } from "../NotificationDot"; import { ListLink, SideNav, SideNavFooter, SideNavHeader } from "../SideNav"; import sideNavStyles from "../SideNav.module.css"; import { StreamListItems } from "../StreamListItems"; -import { AuthErrorDialog } from "./AuthErrorDialog"; import { ChatSidebar } from "./ChatSidebar"; import { Footer } from "./Footer"; import styles from "./index.module.css"; @@ -62,6 +67,14 @@ import { TopRightButtons } from "./TopRightButtons"; const MAX_DESKTOP_FRIENDS = 4; +// lazy loaded so the rarely needed auth error dialog stays out of the eager +// bundle loaded on every page +const AuthErrorDialog = React.lazy(() => + import("./AuthErrorDialog").then((module) => ({ + default: module.AuthErrorDialog, + })), +); + /** Id of the loading-bar track rendered inside the header. NProgress mounts its * bar into it; the track sits just below the header border, spans only the area * between the sidebars, and clips the bar so it never extends over a sidebar. @@ -240,6 +253,7 @@ export function Layout({ const { formatRelativeDate } = useRelativeDayFormat(); const isHydrated = useHydrated(); const location = useLocation(); + const [searchParams] = useSearchParams(); const headerRef = React.useRef(null); const navOffset = useNavOffset(headerRef); @@ -488,7 +502,11 @@ export function Layout({ setChatSidebarOpen(false)} /> ) : null} - + {searchParams.has("authError") ? ( + + + + ) : null} ); } diff --git a/app/config-helpers.ts b/app/config-helpers.server.ts similarity index 100% rename from app/config-helpers.ts rename to app/config-helpers.server.ts diff --git a/app/config.server.ts b/app/config.server.ts index 022a1aef4..cf0772842 100644 --- a/app/config.server.ts +++ b/app/config.server.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { formatEnvErrors, requiredInProd } from "./config-helpers"; +import { formatEnvErrors, requiredInProd } from "./config-helpers.server"; import { IS_E2E_TEST_RUN } from "./utils/e2e"; /** diff --git a/app/config.ts b/app/config.ts index c66ac6fb2..bae212fac 100644 --- a/app/config.ts +++ b/app/config.ts @@ -1,5 +1,3 @@ -import { z } from "zod"; -import { formatEnvErrors, requiredInProd } from "./config-helpers"; import { IS_E2E_TEST_RUN } from "./utils/e2e"; /** @@ -9,6 +7,9 @@ import { IS_E2E_TEST_RUN } from "./utils/e2e"; * Values are validated once when this module is first imported, surfacing a * single clear error for any misconfigured variable. Variables required in * production fall back to development defaults outside of production. + * + * Note: this module ships in the critical client bundle so it must stay free of + * heavy dependencies (e.g. zod, which the server config uses). */ // `import.meta.env` is undefined when Playwright bundles test code, so guard the @@ -23,31 +24,35 @@ const isProd = import.meta.env.PROD === true && !IS_E2E_TEST_RUN; -const schema = z.object({ - VITE_SITE_DOMAIN: requiredInProd(isProd, "http://localhost:5173"), +const TRUTHY_STRINGS = ["true", "1", "yes", "on", "y", "enabled"]; +const FALSY_STRINGS = ["false", "0", "no", "off", "n", "disabled"]; + +const issues: Array<{ name: string; message: string }> = []; + +const values = { + VITE_SITE_DOMAIN: requiredInProd("VITE_SITE_DOMAIN", "http://localhost:5173"), VITE_TOURNAMENT_DEFAULT_LOGO: requiredInProd( - isProd, + "VITE_TOURNAMENT_DEFAULT_LOGO", "tournament-logo-default.avif", ), - VITE_STATIC_ASSETS_URL: z - .string() - .default("https://sendou-assets.nyc3.cdn.digitaloceanspaces.com"), - VITE_PROD_MODE: z.stringbool().default(false), - VITE_SHOW_LUTI_NAV_ITEM: z.stringbool().default(false), - VITE_FUSE_ENABLED: z.stringbool().default(false), - VITE_LEAGUE_GOOGLE_FORM_URL: z.string().optional(), - VITE_SHOW_BANNER_FOR_SEASON: z.string().optional(), - VITE_SENTRY_DSN: z.string().optional(), - VITE_SENTRY_ENABLED: z.stringbool().default(false), - VITE_SKALOP_WS_URL: z.string().optional(), - VITE_VAPID_PUBLIC_KEY: z.string().optional(), -}); + VITE_STATIC_ASSETS_URL: withDefault( + "VITE_STATIC_ASSETS_URL", + "https://sendou-assets.nyc3.cdn.digitaloceanspaces.com", + ), + VITE_PROD_MODE: stringBool("VITE_PROD_MODE"), + VITE_SHOW_LUTI_NAV_ITEM: stringBool("VITE_SHOW_LUTI_NAV_ITEM"), + VITE_FUSE_ENABLED: stringBool("VITE_FUSE_ENABLED"), + VITE_LEAGUE_GOOGLE_FORM_URL: env.VITE_LEAGUE_GOOGLE_FORM_URL, + VITE_SHOW_BANNER_FOR_SEASON: env.VITE_SHOW_BANNER_FOR_SEASON, + VITE_SENTRY_DSN: env.VITE_SENTRY_DSN, + VITE_SENTRY_ENABLED: stringBool("VITE_SENTRY_ENABLED"), + VITE_SKALOP_WS_URL: env.VITE_SKALOP_WS_URL, + VITE_VAPID_PUBLIC_KEY: env.VITE_VAPID_PUBLIC_KEY, +}; -const parsed = schema.safeParse(env); -if (!parsed.success) { - throw formatEnvErrors("client", parsed.error); +if (issues.length > 0) { + throw envError(issues); } -const values = parsed.data; export const Config = { /** Base URL of the site, e.g. `https://sendou.ink`. */ @@ -79,3 +84,51 @@ export const Config = { publicKey: values.VITE_VAPID_PUBLIC_KEY, }, }; + +function requiredInProd(name: string, devFallback: string): string { + const value = env[name]; + + if (!isProd) { + return value ?? devFallback; + } + + if (value === undefined) { + issues.push({ name, message: "required in production" }); + return ""; + } + if (value.length === 0) { + issues.push({ name, message: "required in production (cannot be empty)" }); + return ""; + } + + return value; +} + +function withDefault(name: string, defaultValue: string): string { + return env[name] ?? defaultValue; +} + +function stringBool(name: string): boolean { + const value = env[name]; + if (value === undefined) return false; + + const normalized = value.toLowerCase(); + if (TRUTHY_STRINGS.includes(normalized)) return true; + if (FALSY_STRINGS.includes(normalized)) return false; + + issues.push({ + name, + message: `must be a boolean-like string (e.g. "true" or "false"), got "${value}"`, + }); + return false; +} + +function envError(issues: Array<{ name: string; message: string }>): Error { + const lines = issues.map((issue) => ` - ${issue.name}: ${issue.message}`); + + return new Error( + `Invalid client environment configuration:\n${lines.join( + "\n", + )}\n\nSee .env.example for the full list of variables and how to set them.`, + ); +} diff --git a/app/entry.client.tsx b/app/entry.client.tsx index 7975fb905..f056e5c35 100644 --- a/app/entry.client.tsx +++ b/app/entry.client.tsx @@ -1,39 +1,14 @@ -import * as Sentry from "@sentry/react-router"; -import "@formatjs/intl-durationformat/polyfill.js"; import i18next from "i18next"; import { hydrateRoot } from "react-dom/client"; import { I18nextProvider } from "react-i18next"; import { HydratedRouter } from "react-router/dom"; import { Config } from "~/config"; +import type { LanguageCode } from "~/modules/i18n/config"; import { i18nLoader } from "./modules/i18n/loader"; +import { loadDateFnsLocale } from "./utils/dates"; import { logger } from "./utils/logger"; import { getSessionId } from "./utils/session-id"; -const SENTRY_ENABLED = Config.sentry.enabled; - -const tracing = SENTRY_ENABLED - ? Sentry.reactRouterTracingIntegration({ - useInstrumentationAPI: true, - }) - : null; - -if (SENTRY_ENABLED) { - Sentry.init({ - dsn: Config.sentry.dsn, - sendDefaultPii: false, - integrations: [ - tracing!, - Sentry.thirdPartyErrorFilterIntegration({ - filterKeys: ["sendou-ink"], - behaviour: "drop-error-if-contains-third-party-frames", - }), - ], - enableLogs: true, - tracesSampleRate: 0.1, - tracePropagationTargets: [/^\//], - }); -} - /** Base delays in milliseconds before each retry attempt following the initial request. */ const FETCH_RETRY_DELAYS_MS = [0, 5000, 15000]; /** Random jitter added to each retry delay to avoid a thundering herd against a struggling server. */ @@ -125,16 +100,36 @@ if ("serviceWorker" in navigator) { }); } -i18nLoader() - .then(() => +/** + * Max time hydration waits for Sentry to initialize. If Sentry loads within + * this window its instrumentation is passed to the router (enabling the + * pageload trace); if not, hydration proceeds without it and errors are still + * captured once Sentry finishes loading. + */ +const SENTRY_INIT_TIMEOUT_MS = 1000; + +const sentryPromise = initSentry(); + +// the server rendered with the page language's date-fns locale, so it must be +// cached before hydration to avoid a markup mismatch +Promise.all([ + i18nLoader(), + loadDateFnsLocale(document.documentElement.lang as LanguageCode), + Promise.race([sentryPromise, wait(SENTRY_INIT_TIMEOUT_MS)]), +]) + .then(([, , sentry]) => hydrateRoot( document, { - if (SENTRY_ENABLED && error && error instanceof Error) { - Sentry.captureException(error); + if (error instanceof Error) { + void sentryPromise.then((sentry) => + sentry?.captureException(error), + ); } }} /> @@ -142,3 +137,38 @@ i18nLoader() ), ) .catch(logger.error); + +// Sentry is dynamically imported so its code is not downloaded at all when +// disabled. A load failure (e.g. an ad blocker) must not block hydration, +// hence the catch. +async function initSentry() { + if (!Config.sentry.enabled) return null; + + try { + const Sentry = await import("@sentry/react-router"); + + const tracing = Sentry.reactRouterTracingIntegration({ + useInstrumentationAPI: true, + }); + + Sentry.init({ + dsn: Config.sentry.dsn, + sendDefaultPii: false, + integrations: [ + tracing, + Sentry.thirdPartyErrorFilterIntegration({ + filterKeys: ["sendou-ink"], + behaviour: "drop-error-if-contains-third-party-frames", + }), + ], + enableLogs: true, + tracesSampleRate: 0.1, + tracePropagationTargets: [/^\//], + }); + + return { tracing, captureException: Sentry.captureException }; + } catch (error) { + logger.error("Failed to initialize Sentry", error); + return null; + } +} diff --git a/app/entry.server.tsx b/app/entry.server.tsx index e8dc1f9eb..d0f6084c9 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -20,6 +20,7 @@ import { everyHourAt30, everyTwoMinutes, } from "./routines/list.server"; +import { loadAllDateFnsLocales } from "./utils/dates"; import { logger } from "./utils/logger"; // Reject/cancel all pending promises after 5 seconds @@ -27,6 +28,8 @@ export const streamTimeout = 5000; const SENTRY_ENABLED = Config.sentry.enabled; +const dateFnsLocalesLoaded = loadAllDateFnsLocales(); + async function handleRequest( request: Request, responseStatusCode: number, @@ -34,6 +37,8 @@ async function handleRequest( reactRouterContext: EntryContext, loadContext: RouterContextProvider, ) { + await dateFnsLocalesLoaded; + const callbackName = isbot(request.headers.get("user-agent")) ? "onAllReady" : "onShellReady"; diff --git a/app/features/build-analyzer/analyzer-hooks.ts b/app/features/build-analyzer/analyzer-hooks.ts index 580577388..ff96be030 100644 --- a/app/features/build-analyzer/analyzer-hooks.ts +++ b/app/features/build-analyzer/analyzer-hooks.ts @@ -10,12 +10,12 @@ import { isAbility } from "~/modules/in-game-lists/utils"; import invariant from "~/utils/invariant"; import { MAX_LDE_INTENSITY } from "./analyzer-constants"; import type { SpecialEffectType } from "./analyzer-types"; +import { serializeBuild } from "./core/serializer"; import { applySpecialEffects, SPECIAL_EFFECTS } from "./core/specialEffects"; import { buildStats } from "./core/stats"; import { buildIsEmpty, buildToAbilityPoints, - serializeBuild, validatedBuildFromSearchParams, validatedWeaponIdFromSearchParams, } from "./core/utils"; diff --git a/app/features/build-analyzer/core/serializer.ts b/app/features/build-analyzer/core/serializer.ts new file mode 100644 index 000000000..777b1f651 --- /dev/null +++ b/app/features/build-analyzer/core/serializer.ts @@ -0,0 +1,15 @@ +import type { BuildAbilitiesTupleWithUnknown } from "~/modules/in-game-lists/types"; +import { UNKNOWN_SHORT } from "../analyzer-constants"; + +/** + * Serializes a build to a comma separated string for use in URLs. + * + * Lives in its own module (instead of `core/utils.ts`) so that `~/utils/urls.ts` + * can import it without pulling the weapon params data into the eager bundle. + */ +export function serializeBuild(build: BuildAbilitiesTupleWithUnknown) { + return build + .flat() + .map((ability) => (ability === "UNKNOWN" ? UNKNOWN_SHORT : ability)) + .join(","); +} diff --git a/app/features/build-analyzer/core/utils.ts b/app/features/build-analyzer/core/utils.ts index 18312a3f0..73626f0ee 100644 --- a/app/features/build-analyzer/core/utils.ts +++ b/app/features/build-analyzer/core/utils.ts @@ -322,13 +322,6 @@ export function validatedBuildFromSearchParams( } } -export function serializeBuild(build: BuildAbilitiesTupleWithUnknown) { - return build - .flat() - .map((ability) => (ability === "UNKNOWN" ? UNKNOWN_SHORT : ability)) - .join(","); -} - export const hpDivided = (hp: number) => hp / 10; export function possibleApValues() { diff --git a/app/features/sendouq/core/SendouQ.server.test.ts b/app/features/sendouq/core/SendouQ.server.test.ts index 8762a9c43..957dd6a0f 100644 --- a/app/features/sendouq/core/SendouQ.server.test.ts +++ b/app/features/sendouq/core/SendouQ.server.test.ts @@ -712,6 +712,10 @@ describe("SendouQ", () => { await createGroup([2]); await createGroup([3]); await createGroup([4]); + await db + .updateTable("Group") + .set({ latestActionAt: databaseTimestampNow() }) + .execute(); await refreshSendouQInstance(); diff --git a/app/features/tier-list-maker/routes/tier-list-maker.tsx b/app/features/tier-list-maker/routes/tier-list-maker.tsx index 47665f5cb..54f5150da 100644 --- a/app/features/tier-list-maker/routes/tier-list-maker.tsx +++ b/app/features/tier-list-maker/routes/tier-list-maker.tsx @@ -9,7 +9,6 @@ import { useSensors, } from "@dnd-kit/core"; import { sortableKeyboardCoordinates } from "@dnd-kit/sortable"; -import { snapdom } from "@zumer/snapdom"; import clsx from "clsx"; import { HardDriveDownload, Plus, RefreshCcw } from "lucide-react"; import { useRef } from "react"; @@ -138,6 +137,8 @@ function TierListMakerContent() { const handleDownload = async () => { if (!tierListRef.current) return; + const { snapdom } = await import("@zumer/snapdom"); + flushSync(() => setScreenshotMode(true)); await snapdom.download(tierListRef.current, { diff --git a/app/modules/i18n/useChangeLanguage.ts b/app/modules/i18n/useChangeLanguage.ts index 1249b2078..2bf2dad9e 100644 --- a/app/modules/i18n/useChangeLanguage.ts +++ b/app/modules/i18n/useChangeLanguage.ts @@ -1,15 +1,23 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; +import type { LanguageCode } from "~/modules/i18n/config"; +import { loadDateFnsLocale } from "~/utils/dates"; /** * Detect when the locale returned by the root route loader changes and call * `i18n.changeLanguage` with the new locale so translations load automatically. + * The date-fns locale is loaded first so the re-render triggered by the + * language change already has it available. * * Vendored from remix-i18next (removed in v8). */ export function useChangeLanguage(locale: string) { const { i18n } = useTranslation(); React.useEffect(() => { - if (i18n.language !== locale) i18n.changeLanguage(locale); + if (i18n.language !== locale) { + void loadDateFnsLocale(locale as LanguageCode).then(() => + i18n.changeLanguage(locale), + ); + } }, [locale, i18n]); } diff --git a/app/utils/dates.ts b/app/utils/dates.ts index 453d2dfbf..e3dc8f036 100644 --- a/app/utils/dates.ts +++ b/app/utils/dates.ts @@ -5,44 +5,67 @@ import { } from "@internationalized/date"; import type { Locale } from "date-fns"; import { formatDistanceToNow as dateFnsFormatDistanceToNow } from "date-fns"; -import { da } from "date-fns/locale/da"; -import { de } from "date-fns/locale/de"; import { enUS } from "date-fns/locale/en-US"; -import { es } from "date-fns/locale/es"; -import { fr } from "date-fns/locale/fr"; -import { frCA } from "date-fns/locale/fr-CA"; -import { he } from "date-fns/locale/he"; -import { it } from "date-fns/locale/it"; -import { ja } from "date-fns/locale/ja"; -import { ko } from "date-fns/locale/ko"; -import { nl } from "date-fns/locale/nl"; -import { pl } from "date-fns/locale/pl"; -import { ptBR } from "date-fns/locale/pt-BR"; -import { ru } from "date-fns/locale/ru"; -import { zhCN } from "date-fns/locale/zh-CN"; import type { MonthYear } from "~/features/plus-voting/core"; import type { LanguageCode } from "~/modules/i18n/config"; +import { logger } from "./logger"; import type { DayMonthYear } from "./zod"; -const LOCALE_MAP: Record = { - da, - de, - en: enUS, - "es-ES": es, - "es-US": es, - "fr-CA": frCA, - "fr-EU": fr, - he, - it, - ja, - ko, - nl, - pl, - "pt-BR": ptBR, - ru, - zh: zhCN, +// en-US ships with date-fns core as the default locale, so it costs no extra bytes +const LOCALE_LOADERS: Record Promise> = { + da: () => import("date-fns/locale/da").then((module) => module.da), + de: () => import("date-fns/locale/de").then((module) => module.de), + en: () => Promise.resolve(enUS), + "es-ES": () => import("date-fns/locale/es").then((module) => module.es), + "es-US": () => import("date-fns/locale/es").then((module) => module.es), + "fr-CA": () => import("date-fns/locale/fr-CA").then((module) => module.frCA), + "fr-EU": () => import("date-fns/locale/fr").then((module) => module.fr), + he: () => import("date-fns/locale/he").then((module) => module.he), + it: () => import("date-fns/locale/it").then((module) => module.it), + ja: () => import("date-fns/locale/ja").then((module) => module.ja), + ko: () => import("date-fns/locale/ko").then((module) => module.ko), + nl: () => import("date-fns/locale/nl").then((module) => module.nl), + pl: () => import("date-fns/locale/pl").then((module) => module.pl), + "pt-BR": () => import("date-fns/locale/pt-BR").then((module) => module.ptBR), + ru: () => import("date-fns/locale/ru").then((module) => module.ru), + zh: () => import("date-fns/locale/zh-CN").then((module) => module.zhCN), }; +const loadedLocales = new Map(); + +/** + * Loads the date-fns locale for the given language into the in-memory cache + * used by {@link formatDistanceToNow}. Load failures are logged and result in + * an English fallback instead of rejecting. + */ +export async function loadDateFnsLocale(language: LanguageCode) { + if (loadedLocales.has(language)) return; + + const loader = LOCALE_LOADERS[language]; + if (!loader) return; + + try { + loadedLocales.set(language, await loader()); + } catch (error) { + logger.warn( + `Failed to load date-fns locale for language ${language}`, + error, + ); + } +} + +/** Loads every date-fns locale into the cache (meant for the server where bundle size does not matter). */ +export function loadAllDateFnsLocales() { + return Promise.all( + (Object.keys(LOCALE_LOADERS) as LanguageCode[]).map(loadDateFnsLocale), + ); +} + +/** + * Formats how long ago / until the given date in the given language. The + * language's date-fns locale must be loaded first via + * {@link loadDateFnsLocale}; otherwise falls back to English. + */ export function formatDistanceToNow( date: Parameters[0], options: Omit< @@ -52,7 +75,7 @@ export function formatDistanceToNow( ) { return dateFnsFormatDistanceToNow(date, { ...options, - locale: LOCALE_MAP[options.language], + locale: loadedLocales.get(options.language) ?? enUS, }); } diff --git a/app/utils/urls.ts b/app/utils/urls.ts index 68366ab81..fff7581e2 100644 --- a/app/utils/urls.ts +++ b/app/utils/urls.ts @@ -3,7 +3,7 @@ import { Config } from "~/config"; import type { GearType, Preference, Tables } from "~/db/tables"; import type { ArtSource } from "~/features/art/art-types"; import type { AuthErrorCode } from "~/features/auth/core/errors"; -import { serializeBuild } from "~/features/build-analyzer/core/utils"; +import { serializeBuild } from "~/features/build-analyzer/core/serializer"; import type { CalendarFilters } from "~/features/calendar/calendar-types"; import type { MapPool } from "~/features/map-list-generator/core/map-pool"; import type { StageBackgroundStyle } from "~/features/map-planner"; diff --git a/package.json b/package.json index 64ccb81d7..73751cf0e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "@dnd-kit/utilities": "3.2.2", "@epic-web/cachified": "5.6.3", "@faker-js/faker": "10.5.0", - "@formatjs/intl-durationformat": "0.10.16", "@internationalized/date": "3.12.2", "@react-router/node": "8.2.0", "@react-router/serve": "8.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a8d0484d..c2e83ce80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,9 +39,6 @@ importers: '@faker-js/faker': specifier: 10.5.0 version: 10.5.0 - '@formatjs/intl-durationformat': - specifier: 0.10.16 - version: 0.10.16 '@internationalized/date': specifier: 3.12.2 version: 3.12.2 @@ -638,18 +635,6 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@formatjs/bigdecimal@0.2.6': - resolution: {integrity: sha512-aPzKsGQOkQRHUEbyO/ZtYfr4EqaBQnSs6U4tzTla1xBnIdEHgY2GqEqso28UMwWRkzKqqTj5+/6BmuOsRkfn2A==} - - '@formatjs/fast-memoize@3.1.6': - resolution: {integrity: sha512-H5aexk1Le7T9TPmscacZ+1pR6CTa2n1wq+HDVGXhH8TzUlQQpeXzZs91dRtmFHrbeNbjPFPfQujUqm7MHgVoXQ==} - - '@formatjs/intl-durationformat@0.10.16': - resolution: {integrity: sha512-Ponn8K7eQAA7qeZRRD85lrvsByfeRDxw6aEE9EXapQ47wknptM0aYgumImVsMaBdJikpWYryGAMRZIB7IYYfmQ==} - - '@formatjs/intl-localematcher@0.8.11': - resolution: {integrity: sha512-MzBbR9ifqgtzHwUl2PGEDtRVZGXF9nXPtrMyN6/iKbQaim1Ob7fnPJQMllVkHm3RK74o5VqnLrZIE9egWJhf5g==} - '@internationalized/date@3.12.2': resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} @@ -5299,19 +5284,6 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@formatjs/bigdecimal@0.2.6': {} - - '@formatjs/fast-memoize@3.1.6': {} - - '@formatjs/intl-durationformat@0.10.16': - dependencies: - '@formatjs/bigdecimal': 0.2.6 - '@formatjs/intl-localematcher': 0.8.11 - - '@formatjs/intl-localematcher@0.8.11': - dependencies: - '@formatjs/fast-memoize': 3.1.6 - '@internationalized/date@3.12.2': dependencies: '@swc/helpers': 0.5.23 diff --git a/types/intl-duration-format.d.ts b/types/intl-duration-format.d.ts deleted file mode 100644 index c69b12a88..000000000 --- a/types/intl-duration-format.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -declare namespace Intl { - interface DurationFormatOptions { - style?: "long" | "short" | "narrow" | "digital"; - } - - interface DurationInput { - years?: number; - months?: number; - weeks?: number; - days?: number; - hours?: number; - minutes?: number; - seconds?: number; - milliseconds?: number; - microseconds?: number; - nanoseconds?: number; - } - - class DurationFormat { - constructor(locales?: string | string[], options?: DurationFormatOptions); - format(duration: DurationInput): string; - } -} diff --git a/vite.config.ts b/vite.config.ts index c970e7952..0b5d26996 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -57,6 +57,15 @@ export default defineConfig((config) => { telemetry: false, unstable_sentryVitePluginOptions: { applicationKey: "sendou-ink", + // tree-shakes SDK features we don't use (Replay, debug logging) + // out of the dynamically imported client bundle + bundleSizeOptimizations: { + excludeDebugStatements: true, + excludeReplayCanvas: true, + excludeReplayShadowDom: true, + excludeReplayIframe: true, + excludeReplayWorker: true, + }, }, }, config,