diff --git a/packages/client/app/components/auth/auth-form.tsx b/packages/client/app/components/auth/auth-form.tsx index 1b0a17e..bc8c058 100644 --- a/packages/client/app/components/auth/auth-form.tsx +++ b/packages/client/app/components/auth/auth-form.tsx @@ -58,9 +58,9 @@ export const AuthForm = ({ error }: AuthFormProps) => { id="password" name="password" type="password" - placeholder="••••••••••" + placeholder="········" required - className="h-11 text-base" + className="h-11 text-base placeholder:font-extrabold" /> diff --git a/packages/client/app/components/auth/sign-up-form.tsx b/packages/client/app/components/auth/sign-up-form.tsx index c8f087a..1c7ab90 100644 --- a/packages/client/app/components/auth/sign-up-form.tsx +++ b/packages/client/app/components/auth/sign-up-form.tsx @@ -55,9 +55,9 @@ export const SignUpForm = ({ error, success }: SignUpFormProps) => { id="password" name="password" type="password" - placeholder="••••••••" + placeholder="········" required - className="h-11 text-base" + className="h-11 text-base placeholder:font-extrabold" /> diff --git a/packages/client/app/components/marketing/testimonial.tsx b/packages/client/app/components/marketing/testimonial.tsx index e83a854..56a9b93 100644 --- a/packages/client/app/components/marketing/testimonial.tsx +++ b/packages/client/app/components/marketing/testimonial.tsx @@ -1,18 +1,52 @@ +import { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; + export const Testimonial = () => { + const testimonials = [ + "Data-River has completely transformed how my team and I handle automation. The visual editor makes complex workflows accessible, even for those with no coding experience. It's intuitive, powerful, and simply brilliant!", + "The modular design of Data-River is perfect for our growing team. We can build, share, and customize blocks, which saves us hours on repetitive tasks. Plus, our non-tech members love the easy drag-and-drop setup.", + "As an educator, Data-River is a game changer. Its intuitive, visual approach makes it easy to introduce programming concepts to students, and the platform’s flexibility allows them to explore more advanced logic when they’re ready.", + ]; + + const [testimonial, setTestimonial] = useState(testimonials[0]); + + useEffect(() => { + const randomIndex = Math.floor(Math.random() * testimonials.length); + setTestimonial(testimonials[randomIndex]); + + const interval = setInterval(() => { + setTestimonial((current) => { + const currentIndex = testimonials.indexOf(current); + const nextIndex = (currentIndex + 1) % testimonials.length; + return testimonials[nextIndex]; + }); + }, 8000); + + return () => clearInterval(interval); + }, []); + return (
-
-

- "I just learned about Data River and I'm in love! It's an open - source Firebase alternative with real-time database changes and - simple UI for database interaction." -

-
- Sarah Chen - • Software Engineer -
-
+ + + + "{testimonial}" + + +
); diff --git a/packages/client/app/routes/_app.profile.$userName.tsx b/packages/client/app/routes/_app.profile.$userName.tsx index 83e2788..2472194 100644 --- a/packages/client/app/routes/_app.profile.$userName.tsx +++ b/packages/client/app/routes/_app.profile.$userName.tsx @@ -1,7 +1,7 @@ import { useLoaderData } from "@remix-run/react"; import { type LoaderFunctionArgs, json, redirect } from "@remix-run/node"; import { getSession } from "~/utils/session.server"; -import { supabase } from "~/utils/supabase.server"; +import { createClient } from "~/utils/supabase.server"; import type { Database } from "~/types/supabase"; import { ProfileHeader } from "~/components/profile/profile-header"; import { AchievementsCard } from "~/components/profile/achievements-card"; @@ -18,6 +18,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return redirect("/profile"); } + const { supabase } = await createClient(request); + const { data: profile, error } = await supabase .from("profiles") .select("*") diff --git a/packages/client/app/routes/_app.settings.profile.avatar.tsx b/packages/client/app/routes/_app.settings.profile.avatar.tsx index 57e43f4..5020862 100644 --- a/packages/client/app/routes/_app.settings.profile.avatar.tsx +++ b/packages/client/app/routes/_app.settings.profile.avatar.tsx @@ -4,7 +4,7 @@ import { unstable_parseMultipartFormData, } from "@remix-run/node"; import { getSession } from "~/utils/session.server"; -import { supabase } from "~/utils/supabase.server"; +import { createClient } from "~/utils/supabase.server"; import { uploadHandler } from "~/utils/upload.server"; import { deleteStorageFile } from "~/utils/storage.server"; @@ -13,7 +13,6 @@ const AVATARS_BUCKET = "avatars"; async function handleAvatarUpload( request: Request, userId: string, - accessToken: string, oldAvatarUrl: string | null, ) { let newAvatarUrl: string | null = null; @@ -21,12 +20,13 @@ async function handleAvatarUpload( try { const formData = await unstable_parseMultipartFormData( request, - uploadHandler(accessToken), + uploadHandler(request), ); newAvatarUrl = formData.get("avatar") as string; // Update profile with new avatar URL + const { supabase } = await createClient(request); const { error: updateError } = await supabase .from("profiles") .update({ @@ -40,13 +40,13 @@ async function handleAvatarUpload( } // Delete old avatar if it exists - await deleteStorageFile(oldAvatarUrl, accessToken, AVATARS_BUCKET); + await deleteStorageFile(request, oldAvatarUrl, AVATARS_BUCKET); return json({ success: true }); } catch (error) { // If we failed after uploading the new avatar, clean it up if (newAvatarUrl) { - await deleteStorageFile(newAvatarUrl, accessToken, AVATARS_BUCKET); + await deleteStorageFile(request, newAvatarUrl, AVATARS_BUCKET); } return json( @@ -61,11 +61,12 @@ async function handleAvatarUpload( } async function handleAvatarRemoval( + request: Request, userId: string, oldAvatarUrl: string | null, - accessToken: string, ) { try { + const { supabase } = await createClient(request); const { error } = await supabase .from("profiles") .update({ avatar_url: null, updated_at: new Date().toISOString() }) @@ -76,7 +77,7 @@ async function handleAvatarRemoval( } // Delete the old avatar file - await deleteStorageFile(oldAvatarUrl, accessToken, AVATARS_BUCKET); + await deleteStorageFile(request, oldAvatarUrl, AVATARS_BUCKET); return json({ success: true }); } catch (error) { @@ -98,6 +99,7 @@ export async function action({ request }: ActionFunctionArgs) { const userId = session.get("user_id") as string; // Fetch current profile to get the old avatar URL + const { supabase } = await createClient(request); const { data: profile, error: profileError } = await supabase .from("profiles") .select("avatar_url") @@ -120,10 +122,10 @@ export async function action({ request }: ActionFunctionArgs) { ) { const formData = await request.formData(); if (formData.get("_action") === "remove") { - return handleAvatarRemoval(userId, oldAvatarUrl, accessToken); + return handleAvatarRemoval(request, userId, oldAvatarUrl); } } // Handle avatar upload - return handleAvatarUpload(request, userId, accessToken, oldAvatarUrl); + return handleAvatarUpload(request, userId, oldAvatarUrl); } diff --git a/packages/client/app/routes/_app.settings.profile.tsx b/packages/client/app/routes/_app.settings.profile.tsx index ebf7a42..a4fa269 100644 --- a/packages/client/app/routes/_app.settings.profile.tsx +++ b/packages/client/app/routes/_app.settings.profile.tsx @@ -6,7 +6,7 @@ import { redirect, } from "@remix-run/node"; import { getSession } from "~/utils/session.server"; -import { supabase } from "~/utils/supabase.server"; +import { createClient } from "~/utils/supabase.server"; import { Card, CardContent, @@ -31,6 +31,7 @@ export async function loader({ request }: LoaderFunctionArgs) { const session = await getSession(request); const userId = session.get("user_id") as string; + const { supabase } = await createClient(request); const { data: profile, error } = await supabase .from("profiles") .select("*") @@ -76,6 +77,7 @@ export async function action({ request }: ActionFunctionArgs) { ); } + const { supabase } = await createClient(request); const { error } = await supabase .from("profiles") .update({ diff --git a/packages/client/app/routes/_app.tsx b/packages/client/app/routes/_app.tsx index 89e0b29..2d4af7a 100644 --- a/packages/client/app/routes/_app.tsx +++ b/packages/client/app/routes/_app.tsx @@ -1,10 +1,11 @@ import { Outlet, useLoaderData } from "@remix-run/react"; import { type LoaderFunctionArgs, json, redirect } from "@remix-run/node"; import { getSession } from "~/utils/session.server"; -import { supabase } from "~/utils/supabase.server"; +import { createClient } from "~/utils/supabase.server"; import type { Database } from "~/types/supabase"; import { Navbar } from "~/components/layout/navbar"; import { CookieConsent } from "~/components/layout/cookie-consent"; +import { isRedirectResponse } from "@remix-run/react/dist/data"; type LoaderData = { profile: Pick< @@ -14,27 +15,21 @@ type LoaderData = { }; export async function loader({ request }: LoaderFunctionArgs) { - const session = await getSession(request.headers.get("Cookie")); - - if (!session.has("access_token")) { - // TODO: Remove this after testing - return json({ - profile: { - id: "test", - display_name: "test", - avatar_url: "test", - username: "test", - }, - }); - // return redirect("/sign-in"); - } + const { supabase } = await createClient(request); + + let { + data: { user }, + } = await supabase.auth.getUser(); - const userId = session.get("user_id") as string; + if (!user) { + console.log("redirecting to sign-in"); + return redirect("/sign-in"); + } const { data: profile, error } = await supabase .from("profiles") .select("*") - .eq("id", userId) + .eq("id", user.id) .single(); if (error || !profile) { diff --git a/packages/client/app/routes/auth.callback.tsx b/packages/client/app/routes/auth.callback.tsx index 21d906b..a4c5fe1 100644 --- a/packages/client/app/routes/auth.callback.tsx +++ b/packages/client/app/routes/auth.callback.tsx @@ -4,7 +4,7 @@ import { type ActionFunctionArgs, json, } from "@remix-run/node"; -import { supabase } from "~/utils/supabase.server"; +import { createClient } from "~/utils/supabase.server"; import { getSession, commitSession } from "~/utils/session.server"; export async function loader({ request }: LoaderFunctionArgs) { @@ -13,12 +13,21 @@ export async function loader({ request }: LoaderFunctionArgs) { const next = url.searchParams.get("next") || "/editor"; if (code) { + const { supabase } = await createClient(request); + const { data, error } = await supabase.auth.exchangeCodeForSession(code); + if (error) { + console.error("Error exchanging code for session", error); + return redirect(`/sign-in?error=${error.message}`); + } + if (!error && data.session) { const session = await getSession(request.headers.get("Cookie")); - session.set("access_token", data.session.access_token); session.set("user_id", data.session.user.id); + session.set("expires_at", data.session.expires_at); + session.set("access_token", data.session.access_token); + session.set("refresh_token", data.session.refresh_token); return redirect(next, { headers: { @@ -35,6 +44,8 @@ export async function action({ request }: ActionFunctionArgs) { const { access_token } = await request.json(); if (access_token) { + const { supabase } = await createClient(request); + const { data: { user }, error, diff --git a/packages/client/app/routes/auth.github.tsx b/packages/client/app/routes/auth.github.tsx index df5c18e..94c95b4 100644 --- a/packages/client/app/routes/auth.github.tsx +++ b/packages/client/app/routes/auth.github.tsx @@ -1,8 +1,10 @@ import type { ActionFunctionArgs } from "@remix-run/node"; import { redirect } from "@remix-run/node"; -import { supabase } from "~/utils/supabase.server"; +import { createClient } from "~/utils/supabase.server"; export async function action({ request }: ActionFunctionArgs) { + const { supabase, headers } = await createClient(request); + const { data, error } = await supabase.auth.signInWithOAuth({ provider: "github", options: { @@ -12,5 +14,7 @@ export async function action({ request }: ActionFunctionArgs) { if (error) throw error; - return redirect(data.url); + return redirect(data.url, { + headers, + }); } diff --git a/packages/client/app/routes/forgot-password.tsx b/packages/client/app/routes/forgot-password.tsx index a97859b..418819d 100644 --- a/packages/client/app/routes/forgot-password.tsx +++ b/packages/client/app/routes/forgot-password.tsx @@ -7,7 +7,7 @@ import { json, redirect } from "@remix-run/node"; import { Card, CardHeader, CardContent } from "@data-river/shared/ui"; import { AuthLayout } from "~/components/layout/auth-layout"; import { ForgotPasswordForm } from "~/components/auth/forgot-password-form"; -import { supabase } from "~/utils/supabase.server"; +import { createClient } from "~/utils/supabase.server"; import { getSession } from "~/utils/session.server"; import { useActionData } from "@remix-run/react"; @@ -38,6 +38,7 @@ export async function action({ request }: ActionFunctionArgs) { const email = formData.get("email") as string; try { + const { supabase } = await createClient(request); const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo: `${new URL(request.url).origin}/reset-password`, }); diff --git a/packages/client/app/routes/sign-in.tsx b/packages/client/app/routes/sign-in.tsx index bba324c..b5b2ce4 100644 --- a/packages/client/app/routes/sign-in.tsx +++ b/packages/client/app/routes/sign-in.tsx @@ -7,7 +7,7 @@ import { json, redirect } from "@remix-run/node"; import { Card, CardHeader, CardContent } from "@data-river/shared/ui"; import { AuthLayout } from "~/components/layout/auth-layout"; import { AuthForm } from "~/components/auth/auth-form"; -import { supabase } from "~/utils/supabase.server"; +import { createClient } from "~/utils/supabase.server"; import { getSession, commitSession } from "~/utils/session.server"; import { useActionData } from "@remix-run/react"; import { useEffect } from "react"; @@ -21,9 +21,13 @@ export const meta: MetaFunction = () => { }; export async function loader({ request }: LoaderFunctionArgs) { - const session = await getSession(request.headers.get("Cookie")); + const { supabase } = await createClient(request, true); - if (session.has("access_token")) { + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (user) { return redirect("/editor"); } @@ -36,6 +40,7 @@ export async function action({ request }: ActionFunctionArgs) { const password = formData.get("password") as string; try { + const { supabase } = await createClient(request, true); const { data, error } = await supabase.auth.signInWithPassword({ email, password, @@ -46,6 +51,8 @@ export async function action({ request }: ActionFunctionArgs) { const session = await getSession(request.headers.get("Cookie")); session.set("access_token", data.session.access_token); session.set("user_id", data.user.id); + session.set("refresh_token", data.session.refresh_token); + session.set("expires_at", data.session.expires_at); return redirect("/editor", { headers: { diff --git a/packages/client/app/routes/sign-up.tsx b/packages/client/app/routes/sign-up.tsx index a8bbe19..e50cabc 100644 --- a/packages/client/app/routes/sign-up.tsx +++ b/packages/client/app/routes/sign-up.tsx @@ -14,7 +14,7 @@ import { } from "@data-river/shared/ui"; import { AuthLayout } from "~/components/layout/auth-layout"; import { SignUpForm } from "~/components/auth/sign-up-form"; -import { supabase } from "~/utils/supabase.server"; +import { createClient } from "~/utils/supabase.server"; import { getSession, commitSession } from "~/utils/session.server"; import { useActionData } from "@remix-run/react"; import { useState } from "react"; @@ -41,6 +41,7 @@ export async function loader({ request }: LoaderFunctionArgs) { const accessToken = session.get("access_token"); if (accessToken) { + const { supabase } = await createClient(request); const { data: { user }, } = await supabase.auth.getUser(accessToken); @@ -73,6 +74,8 @@ export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const username = formData.get("username") as string; + const { supabase } = await createClient(request); + // Handle username selection if (username) { try { @@ -185,7 +188,6 @@ export async function action({ request }: ActionFunctionArgs) { const SignUpPage = () => { const actionData = useActionData(); - const navigate = useNavigate(); // Show username selection form if (actionData?.step === "username") { diff --git a/packages/client/app/utils/storage.server.ts b/packages/client/app/utils/storage.server.ts index 7c2ad37..ded7e81 100644 --- a/packages/client/app/utils/storage.server.ts +++ b/packages/client/app/utils/storage.server.ts @@ -1,14 +1,14 @@ -import { createServerClient } from "./supabase.server"; +import { createClient } from "./supabase.server"; export async function deleteStorageFile( + request: Request, path: string | null, - accessToken: string, bucket: string, ) { if (!path) return; try { - const supabase = createServerClient(accessToken); + const { supabase } = await createClient(request); const { error } = await supabase.storage .from(bucket) .remove([getFileNameFromUrl(path)]); diff --git a/packages/client/app/utils/supabase.server.ts b/packages/client/app/utils/supabase.server.ts index 900523b..a864f9f 100644 --- a/packages/client/app/utils/supabase.server.ts +++ b/packages/client/app/utils/supabase.server.ts @@ -1,14 +1,49 @@ -import { createClient } from "@supabase/supabase-js"; +import { + createServerClient, + parseCookieHeader, + serializeCookieHeader, +} from "@supabase/ssr"; import { type Database } from "~/types/supabase"; +import { getSession } from "./session.server"; if (!process.env.SUPABASE_URL) throw new Error("Missing SUPABASE_URL"); if (!process.env.SUPABASE_ANON_KEY) throw new Error("Missing SUPABASE_ANON_KEY"); -export const supabase = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY, -); +export async function createClient(request: Request, skipRefresh = false) { + const headers = new Headers(); -export const createServerClient = (accessToken: string) => - createClient(process.env.SUPABASE_URL!, accessToken); + const supabase = await createServerClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return parseCookieHeader(request.headers.get("Cookie") ?? ""); + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => + headers.append( + "Set-Cookie", + serializeCookieHeader(name, value, options), + ), + ); + }, + }, + }, + ); + + let errorRefreshingSession: Error | null = null; + if (!skipRefresh) { + const session = await getSession(request.headers.get("Cookie")); + + const { error } = await supabase.auth.setSession({ + access_token: session.get("access_token") as string, + refresh_token: session.get("refresh_token") as string, + }); + + if (error) errorRefreshingSession = error; + } + + return { supabase, headers, errorRefreshingSession }; +} diff --git a/packages/client/app/utils/upload.server.ts b/packages/client/app/utils/upload.server.ts index 67decf7..2ee8425 100644 --- a/packages/client/app/utils/upload.server.ts +++ b/packages/client/app/utils/upload.server.ts @@ -1,5 +1,5 @@ import type { UploadHandler } from "@remix-run/node"; -import { createServerClient } from "./supabase.server"; +import { createClient } from "./supabase.server"; import { v4 as uuidv4 } from "uuid"; import sharp from "sharp"; @@ -41,18 +41,14 @@ async function compressImage( } export const uploadHandler = - (accessToken: string): UploadHandler => + (request: Request): UploadHandler => async ({ name, contentType, data }) => { if (name !== "avatar") { return undefined; } - if (!accessToken) { - throw new Error("Unauthorized"); - } - // Create an authenticated Supabase client - const supabase = createServerClient(accessToken); + const { supabase } = await createClient(request); // Validate content type if (!Object.keys(ALLOWED_TYPES).includes(contentType)) { @@ -103,8 +99,6 @@ export const uploadHandler = } catch (cleanupError) { console.error("Failed to clean up file after error:", cleanupError); } - - console.error("Upload error:", error); throw new Error("Failed to upload file"); } }; diff --git a/packages/client/package.json b/packages/client/package.json index 0a363fd..b8bad63 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -53,6 +53,7 @@ "@remix-run/router": "^1.20.0", "@remix-run/serve": "^2.13.1", "@remix-run/server-runtime": "^2.13.1", + "@supabase/ssr": "^0.5.1", "@supabase/supabase-js": "^2.45.4", "@types/set-cookie-parser": "^2.4.10", "@web3-storage/multipart-parser": "^1.0.0", diff --git a/packages/client/supabase/migrations/2024110700000_avatar_bucket_null_owner.sql b/packages/client/supabase/migrations/2024110700000_avatar_bucket_null_owner.sql new file mode 100644 index 0000000..6a150f4 --- /dev/null +++ b/packages/client/supabase/migrations/2024110700000_avatar_bucket_null_owner.sql @@ -0,0 +1,36 @@ +begin; -- Start transaction + +-- Drop existing policies +DROP POLICY IF EXISTS "Allow users to delete their own avatar" ON storage.objects; +DROP POLICY IF EXISTS "Allow users to insert their own avatar" ON storage.objects; +DROP POLICY IF EXISTS "Allow users to update their own avatar" ON storage.objects; + +-- Create updated policies that allow actions when owner is null +CREATE POLICY "Allow users to delete their own avatar or null owner" +ON storage.objects +FOR DELETE +TO authenticated +USING ( + bucket_id = 'avatars' AND + (owner IS NULL OR auth.uid() = owner) +); + +CREATE POLICY "Allow users to insert their own avatar or null owner" +ON storage.objects +FOR INSERT +TO authenticated +WITH CHECK ( + bucket_id = 'avatars' AND + (owner IS NULL OR owner = auth.uid()) +); + +CREATE POLICY "Allow users to update their own avatar or null owner" +ON storage.objects +FOR UPDATE +TO authenticated +WITH CHECK ( + bucket_id = 'avatars' AND + (owner IS NULL OR owner = auth.uid()) +); + +commit; -- End transaction \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be9b059..cec3a3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -214,6 +214,9 @@ importers: '@remix-run/server-runtime': specifier: ^2.13.1 version: 2.13.1(typescript@5.6.3) + '@supabase/ssr': + specifier: ^0.5.1 + version: 0.5.1(@supabase/supabase-js@2.45.4) '@supabase/supabase-js': specifier: ^2.45.4 version: 2.45.4 @@ -2990,6 +2993,11 @@ packages: '@supabase/realtime-js@2.10.2': resolution: {integrity: sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==} + '@supabase/ssr@0.5.1': + resolution: {integrity: sha512-+G94H/GZG0nErZ3FQV9yJmsC5Rj7dmcfCAwOt37hxeR1La+QTl8cE9whzYwPUrTJjMLGNXoO+1BMvVxwBAbz4g==} + peerDependencies: + '@supabase/supabase-js': ^2.43.4 + '@supabase/storage-js@2.7.0': resolution: {integrity: sha512-iZenEdO6Mx9iTR6T7wC7sk6KKsoDPLq8rdu5VRy7+JiT1i8fnqfcOr6mfF2Eaqky9VQzhP8zZKQYjzozB65Rig==} @@ -10532,6 +10540,11 @@ snapshots: - bufferutil - utf-8-validate + '@supabase/ssr@0.5.1(@supabase/supabase-js@2.45.4)': + dependencies: + '@supabase/supabase-js': 2.45.4 + cookie: 0.6.0 + '@supabase/storage-js@2.7.0': dependencies: '@supabase/node-fetch': 2.6.15 @@ -12216,7 +12229,7 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 9.12.0(jiti@1.21.6) - eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.12.0(jiti@1.21.6)))(eslint@9.12.0(jiti@1.21.6)) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.12.0(jiti@1.21.6)) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -12229,7 +12242,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.11.0(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.12.0(jiti@1.21.6)))(eslint@9.12.0(jiti@1.21.6)): + eslint-module-utils@2.11.0(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.12.0(jiti@1.21.6)): dependencies: debug: 3.2.7 optionalDependencies: @@ -12239,7 +12252,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.12.0(jiti@1.21.6)))(eslint@9.12.0(jiti@1.21.6)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.12.0(jiti@1.21.6)): dependencies: debug: 3.2.7 optionalDependencies: @@ -12261,7 +12274,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.12.0(jiti@1.21.6) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.12.0(jiti@1.21.6)))(eslint@9.12.0(jiti@1.21.6)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.10.0(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.12.0(jiti@1.21.6)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3