feat: replace apollo with urql

This commit is contained in:
Sylver 2024-02-11 18:21:19 +08:00
parent fc0071165f
commit bdcafc8347
29 changed files with 240 additions and 271 deletions

View File

@ -16,8 +16,6 @@
"watch": "concurrently \"vavite serve\" \"pnpm generate --watch\""
},
"devDependencies": {
"@0no-co/graphqlsp": "^1.3.0",
"@apollo/client": "^3.8.9",
"@atlasbot/configs": "^10.5.15",
"@fastify/early-hints": "^1.0.1",
"@fastify/http-proxy": "^9.3.0",
@ -32,6 +30,8 @@
"@types/node": "^20.10.6",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"@urql/devtools": "^2.0.3",
"@urql/exchange-graphcache": "^6.4.1",
"autoprefixer": "^10.4.16",
"clsx": "^2.1.0",
"concurrently": "^8.2.2",
@ -61,6 +61,7 @@
"tailwindcss": "^3.4.1",
"tsup": "^8.0.1",
"typescript": "^5.3.3",
"urql": "^4.0.6",
"vavite": "^4.0.1",
"vike": "^0.4.156",
"vite": "^5.0.11",

View File

@ -1,5 +1,5 @@
import clsx from 'clsx';
import React, { forwardRef } from 'react';
import { forwardRef } from 'react';
import { FiArrowLeft } from 'react-icons/fi';
export interface BreadcrumbsProps {

View File

@ -1,7 +1,7 @@
/* eslint-disable react/button-has-type */
import clsx from 'clsx';
import type { FC, HTMLAttributes } from 'react';
import React, { forwardRef } from 'react';
import { forwardRef } from 'react';
import { Spinner } from './spinner';
export interface ButtonProps extends Omit<HTMLAttributes<HTMLButtonElement | HTMLAnchorElement>, 'prefix' | 'style'> {

View File

@ -1,6 +1,5 @@
import clsx from 'clsx';
import type { FC, ReactNode } from 'react';
import React from 'react';
export interface ContainerProps {
centerX?: boolean;

View File

@ -13,7 +13,7 @@ import { Link } from '../link';
import { useToasts } from '../toast';
import { HeaderUser } from './header-user';
import { graphql } from '../../@generated';
import { useMutation } from '@apollo/client';
import { useMutation } from 'urql';
const ResendVerificationEmail = graphql(`
mutation ResendVerificationEmail($data: ResendVerificationEmailDto) {
@ -40,7 +40,7 @@ export const Header = memo(() => {
setShowEmailInput(false);
});
const [resendMutation] = useMutation(ResendVerificationEmail);
const [, resendMutation] = useMutation(ResendVerificationEmail);
const [resendVerification, sendingVerification] = useAsync(async () => {
if (resent || !user.data) return;
if (!user.data.email && !email) {
@ -51,9 +51,7 @@ export const Header = memo(() => {
const payload = !user.data.email && email ? { email } : null;
try {
await resendMutation({
variables: {
data: payload,
},
data: payload,
});
setShowEmailInput(false);

View File

@ -1,4 +1,4 @@
import { forwardRef, Fragment, type HTMLAttributes } from 'react';
import { forwardRef, type HTMLAttributes } from 'react';
export interface LinkProps extends HTMLAttributes<HTMLAnchorElement> {
href: string;

View File

@ -1,6 +1,5 @@
import clsx from 'clsx';
import type { FC, HTMLAttributes } from 'react';
import React from 'react';
export interface SpinnerProps extends HTMLAttributes<SVGElement> {
size?: 'small' | 'medium' | 'large';

View File

@ -1,5 +1,5 @@
import React from "react";
import { ToastProps } from "./toast";
import React from 'react';
import { ToastProps } from './toast';
export type ToastContextData = null | ((toast: ToastProps) => void);
export const ToastContext = React.createContext<ToastContextData>(null);

View File

@ -1,9 +1,9 @@
import { nanoid } from 'nanoid';
import type { FC, ReactNode } from 'react';
import React, { Fragment, useCallback, useState } from 'react';
import { useCallback, useState } from 'react';
import { ToastContext } from './context';
import type { ToastProps } from './toast';
import { Toast, TRANSITION_DURATION } from './toast';
import { TRANSITION_DURATION, Toast } from './toast';
// spread operators on arrays are to fix this
// https://stackoverflow.com/questions/56266575/why-is-usestate-not-triggering-re-render

View File

@ -1,6 +1,6 @@
import clsx from 'clsx';
import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
export interface ToastProps {
text: string;

View File

@ -1,6 +1,6 @@
import { useQuery } from '@apollo/client';
import type { FC } from 'react';
import { Fragment } from 'react';
import { useQuery } from 'urql';
import { graphql } from '../../@generated';
import { Breadcrumbs } from '../../components/breadcrumbs';
import { Card } from '../../components/card';
@ -51,9 +51,10 @@ const GetPastesQuery = graphql(`
export const FileList: FC = () => {
const [filter, setFilter] = useQueryState('filter', 'files');
const files = useQuery(GetFilesQuery, { skip: filter !== 'files' });
const pastes = useQuery(GetPastesQuery, { skip: filter !== 'pastes' });
const [files, filesFetchMore] = useQuery({ query: GetFilesQuery, pause: filter !== 'files' });
const [pastes, pastesFetchMore] = useQuery({ query: GetPastesQuery, pause: filter !== 'pastes' });
const source = filter === 'files' ? files : pastes;
const fetchMore = filter === 'files' ? filesFetchMore : pastesFetchMore;
if (source.error) {
return <Error error={source.error} />;
}
@ -102,7 +103,7 @@ export const FileList: FC = () => {
{pastes.data?.user.pastes.edges.map(({ node }) => <PasteCard key={node.id} paste={node} />)}
</div>
)}
{!source.loading && !hasContent && (
{!source.fetching && !hasContent && (
<Card className="text-gray-500">
You haven&apos;t uploaded anything yet. Once you upload something, it will appear here.
</Card>
@ -113,7 +114,7 @@ export const FileList: FC = () => {
className="w-full bg-dark-200 px-2 py-2 text-gray-500 hover:bg-dark-300 transition"
type="button"
onClick={() => {
source.fetchMore({
fetchMore({
variables: {
after: currentPageInfo.pageInfo.endCursor,
},

View File

@ -1,4 +1,4 @@
import { useQuery } from '@apollo/client';
import { CombinedError, useQuery } from 'urql';
import { graphql } from '../@generated';
const ConfigQuery = graphql(`
@ -24,9 +24,9 @@ const ConfigQuery = graphql(`
`);
export const useConfig = () => {
const config = useQuery(ConfigQuery);
const [config] = useQuery({ query: ConfigQuery });
return {
...config,
error: config.error as CombinedError | undefined,
data: config.data?.config,
};
};

View File

@ -1,5 +1,5 @@
import { TypedDocumentNode, useMutation, useQuery } from '@apollo/client';
import { useEffect, useState } from 'react';
import { CombinedError, TypedDocumentNode, useMutation, useQuery } from 'urql';
import { graphql } from '../@generated';
import type { GetUserQuery, LoginMutationVariables, RegularUserFragment } from '../@generated/graphql';
import { navigate, reload } from '../helpers/routing';
@ -38,10 +38,10 @@ const LogoutMutation = graphql(`
export const useLoginUser = () => {
const [otp, setOtp] = useState(false);
const [loginMutation] = useMutation(LoginMutation);
const [, loginMutation] = useMutation(LoginMutation);
const [login] = useAsync(async (variables: LoginMutationVariables) => {
try {
await loginMutation({ variables });
await loginMutation(variables);
navigate('/dashboard');
} catch (error: any) {
if (error.message.toLowerCase().includes('otp')) {
@ -59,7 +59,7 @@ export const useLoginUser = () => {
};
export const useLogoutUser = () => {
const [logoutMutation] = useMutation(LogoutMutation);
const [, logoutMutation] = useMutation(LogoutMutation);
const [logout] = useAsync(async () => {
await logoutMutation({});
reload();
@ -69,27 +69,27 @@ export const useLogoutUser = () => {
};
export const useUserRedirect = (
query: { data: { user: RegularUserFragment } | null | undefined; loading: boolean; called: boolean },
query: { data?: { user: RegularUserFragment } | null | undefined; fetching: boolean },
redirect: boolean | undefined,
) => {
useEffect(() => {
if (!query.data && !query.loading && query.called && redirect) {
if (!query.data && !query.fetching && redirect) {
navigate(`/login?to=${window.location.href}`);
}
}, [redirect, query.data, query.loading, query.called]);
}, [redirect, query.data, query.fetching]);
};
export const useUser = <T extends TypedDocumentNode<GetUserQuery, any>>(redirect?: boolean, query?: T) => {
const { login, otpRequired } = useLoginUser();
const { logout } = useLogoutUser();
const { data, loading, called, error } = useQuery((query || UserQuery) as T);
const [{ data, fetching, error }] = useQuery({ query: (query || UserQuery) as T });
useUserRedirect({ data, loading, called }, redirect);
useUserRedirect({ data, fetching }, redirect);
return {
data: data?.user as RegularUserFragment | null | undefined,
loading: loading,
error: error,
fetching: fetching,
error: error as CombinedError | undefined,
otpRequired: otpRequired,
login: login,
logout: logout,

View File

@ -1,4 +1,4 @@
import { useMutation, useQuery } from '@apollo/client';
import { useMutation, useQuery } from 'urql';
import clsx from 'clsx';
import { QRCodeSVG } from 'qrcode.react';
import { FC, Fragment, useCallback, useMemo } from 'react';
@ -32,10 +32,10 @@ const ConfirmOTP = graphql(`
`);
export const Page: FC = () => {
const result = useQuery(GenerateOtp);
const [result] = useQuery({ query: GenerateOtp });
const createToast = useToasts();
const [currentStep, setCurrentStep] = useQueryState('step', 0, Number);
const [confirmOtp] = useMutation(ConfirmOTP);
const [, confirmOtp] = useMutation(ConfirmOTP);
const copyable = useMemo(() => {
if (!result.data) return;
@ -64,7 +64,7 @@ export const Page: FC = () => {
const [confirm, confirming] = useAsync(async (otpCode: string) => {
try {
await confirmOtp({ variables: { otpCode } });
await confirmOtp({ otpCode });
createToast({ text: 'Successfully enabled 2FA!' });
navigate('/dashboard', { overwriteLastHistoryEntry: true });
} catch (error: any) {
@ -77,7 +77,7 @@ export const Page: FC = () => {
}
});
if (result.loading) return <PageLoader />;
if (result.fetching) return <PageLoader />;
if (!result.data) return <Error error={result.error} />;
return (

View File

@ -1,7 +1,6 @@
import { useMutation, useQuery } from '@apollo/client';
import { FC, Fragment } from 'react';
import { useMutation, useQuery } from 'urql';
import { graphql } from '../../../@generated';
import { GetUserDocument } from '../../../@generated/graphql';
import { Breadcrumbs } from '../../../components/breadcrumbs';
import { Button } from '../../../components/button';
import { Container } from '../../../components/container';
@ -39,22 +38,20 @@ const UserQueryWithToken = graphql(`
`);
export const Page: FC = () => {
const user = useQuery(UserQueryWithToken);
const [user] = useQuery({ query: UserQueryWithToken });
const { logout } = useLogoutUser();
const [refreshMutation] = useMutation(RefreshToken);
const [, refreshMutation] = useMutation(RefreshToken);
const [refresh, refreshing] = useAsync(async () => {
// eslint-disable-next-line no-alert
const confirmation = confirm('Are you sure? This will invalidate all existing configs and sessions and will sign you out of the dashboard.') // prettier-ignore
if (!confirmation) return;
await refreshMutation();
await refreshMutation({});
await logout();
});
useUserRedirect(user, true);
const [disableOTP, disableOTPMut] = useMutation(DisableOtp, {
refetchQueries: [{ query: GetUserDocument }],
});
const [disableOTPMut, disableOTP] = useMutation(DisableOtp);
return (
<Container>
@ -125,11 +122,9 @@ export const Page: FC = () => {
<div className="right flex items-center col-span-full md:col-span-1">
{user.data && user.data.user.otpEnabled && (
<OtpInput
loading={disableOTPMut.loading}
loading={disableOTPMut.fetching}
onCode={(otpCode) => {
disableOTP({
variables: { otpCode },
});
disableOTP({ otpCode });
}}
/>
)}

View File

@ -1,9 +1,9 @@
import { useMutation, useQuery } from '@apollo/client';
import clsx from 'clsx';
import copyToClipboard from 'copy-to-clipboard';
import type { FC, ReactNode } from 'react';
import { Fragment, useState } from 'react';
import { FiDownload, FiShare, FiTrash } from 'react-icons/fi';
import { useMutation, useQuery } from 'urql';
import { graphql } from '../../../@generated';
import { Container } from '../../../components/container';
import { Embed } from '../../../components/embed/embed';
@ -72,14 +72,15 @@ export const Page: FC<PageProps> = ({ routeParams }) => {
const [deleteKey] = useQueryState<string | undefined>('deleteKey');
const [confirm, setConfirm] = useState(false);
const createToast = useToasts();
const file = useQuery(GetFile, {
skip: !fileId,
const [file] = useQuery({
query: GetFile,
pause: !fileId,
variables: {
fileId: fileId as string,
},
});
const [deleteMutation] = useMutation(DeleteFile);
const [, deleteMutation] = useMutation(DeleteFile);
const copyLink = () => {
copyToClipboard(file.data?.file.urls.view ?? window.location.href);
createToast({
@ -100,10 +101,8 @@ export const Page: FC<PageProps> = ({ routeParams }) => {
}
await deleteMutation({
variables: {
fileId: file.data.file.id,
deleteKey: deleteKey,
},
fileId: file.data.file.id,
deleteKey: deleteKey,
});
createToast({ text: `Deleted "${file.data.file.displayName}"` });

View File

@ -1,4 +1,3 @@
import { useMutation, useQuery } from '@apollo/client';
import { FC, useEffect } from 'react';
import { graphql } from '../../../@generated';
import { Container } from '../../../components/container';
@ -14,6 +13,7 @@ import { navigate, prefetch } from '../../../helpers/routing';
import { useAsync } from '../../../hooks/useAsync';
import { useConfig } from '../../../hooks/useConfig';
import { PageProps } from '../../../renderer/types';
import { useQuery, useMutation } from 'urql';
const GetInvite = graphql(`
query GetInvite($inviteId: ID!) {
@ -36,23 +36,21 @@ export const Page: FC<PageProps> = ({ routeParams }) => {
const config = useConfig();
const createToast = useToasts();
const inviteToken = routeParams.inviteToken;
const invite = useQuery(GetInvite, { skip: !inviteToken, variables: { inviteId: inviteToken! } });
const [invite] = useQuery({ query: GetInvite, pause: !inviteToken, variables: { inviteId: inviteToken! } });
const expiresAt = invite.data?.invite.expiresAt;
useEffect(() => {
prefetch('/login');
}, []);
const [createUserMutation] = useMutation(CreateUser);
const [, createUserMutation] = useMutation(CreateUser);
const [onSubmit] = useAsync(async (data: SignupData) => {
try {
if (!inviteToken) return;
await createUserMutation({
variables: {
user: {
...data,
invite: inviteToken,
},
user: {
...data,
invite: inviteToken,
},
});

View File

@ -1,4 +1,3 @@
import { useMutation } from '@apollo/client';
import { Form, Formik } from 'formik';
import { FC } from 'react';
import * as Yup from 'yup';
@ -15,6 +14,7 @@ import { Title } from '../../components/title';
import { encryptContent } from '../../helpers/encrypt.helper';
import { useConfig } from '../../hooks/useConfig';
import { useUser } from '../../hooks/useUser';
import { useMutation } from 'urql';
const EXPIRY_OPTIONS = [
{ name: '15 minutes', value: 15 },
@ -98,7 +98,7 @@ const CreatePaste = graphql(`
export const Page: FC = () => {
const user = useUser();
const config = useConfig();
const [pasteMutation] = useMutation(CreatePaste);
const [, pasteMutation] = useMutation(CreatePaste);
if (user.error) {
return <Error error={user.error} />;
}
@ -148,12 +148,9 @@ export const Page: FC = () => {
}
const paste = await pasteMutation({
variables: {
input: body,
},
input: body,
});
if (paste.errors && paste.errors[0]) throw paste.errors[0];
const url = new URL(paste.data!.createPaste.urls.view);
if (body.burn) url.searchParams.set('burn_unless', user.data.id);
if (encryptionKey) url.hash = `key=${encryptionKey}`;

View File

@ -1,4 +1,3 @@
import { useQuery } from '@apollo/client';
import { FC, useEffect, useState } from 'react';
import { FiBookOpen, FiClock, FiTrash } from 'react-icons/fi';
import { graphql } from '../../../@generated';
@ -14,6 +13,7 @@ import { hashToObject } from '../../../helpers/hash-to-object';
import { navigate } from '../../../helpers/routing';
import { useUser } from '../../../hooks/useUser';
import { PageProps } from '../../../renderer/types';
import { useQuery } from 'urql';
const PasteQuery = graphql(`
query GetPaste($pasteId: ID!) {
@ -47,8 +47,9 @@ export const Page: FC<PageProps> = ({ routeParams }) => {
const skipQuery =
!pasteId || (!confirmedBurn && (burnUnless === undefined || (burnUnless ? burnUnless !== user.data?.id : false)));
const paste = useQuery(PasteQuery, {
skip: skipQuery,
const [paste] = useQuery({
query: PasteQuery,
pause: skipQuery,
variables: {
pasteId: pasteId!,
},

View File

@ -1,4 +1,4 @@
import { useMutation } from '@apollo/client';
import { useMutation } from 'urql';
import { Form, Formik } from 'formik';
import { FC, useState } from 'react';
import * as Yup from 'yup';
@ -30,7 +30,7 @@ const Shorten = graphql(`
`);
export const Page: FC = () => {
const [shortenMutation] = useMutation(Shorten);
const [, shortenMutation] = useMutation(Shorten);
const [result, setResult] = useState<string | null>(null);
const config = useConfig();
@ -48,10 +48,8 @@ export const Page: FC = () => {
validationSchema={schema}
onSubmit={async (values) => {
const result = await shortenMutation({
variables: {
link: values.url,
host: values.host,
},
link: values.url,
host: values.host,
});
setResult(result.data!.createLink.urls.view);

View File

@ -1,32 +1,42 @@
import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache } from '@apollo/client';
import { createClient, fetchExchange, ssrExchange } from 'urql';
import { Provider as UrqlProvider } from 'urql';
import { hydrateRoot } from 'react-dom/client';
import { HelmetProvider } from 'react-helmet-async';
import { OnRenderClientAsync } from 'vike/types';
import { App } from '../app';
import { typePolicies } from './policy';
import { PageContextProvider } from './usePageContext';
import { HelmetProvider } from 'react-helmet-async';
import { cacheOptions } from './cache';
import { cacheExchange } from '@urql/exchange-graphcache';
export const onRenderClient: OnRenderClientAsync = async (pageContext) => {
const { Page } = pageContext;
const client = new ApolloClient({
link: new HttpLink({ uri: '/api/graphql' }),
cache: new InMemoryCache({ typePolicies }),
const ssr = ssrExchange({ isClient: true });
const exchanges = [ssr, cacheExchange(cacheOptions), fetchExchange];
if (import.meta.env.MODE === 'development') {
const { devtoolsExchange } = await import('@urql/devtools');
exchanges.unshift(devtoolsExchange);
}
const client = createClient({
url: '/api/graphql',
exchanges: exchanges,
});
if (pageContext.state) {
client.cache.restore(pageContext.state);
ssr.restoreData(pageContext.state);
}
hydrateRoot(
document.getElementById('root')!,
<PageContextProvider pageContext={pageContext}>
<ApolloProvider client={client}>
<UrqlProvider value={client}>
<HelmetProvider>
<App>
<Page routeParams={pageContext.routeParams || {}} />
</App>
</HelmetProvider>
</ApolloProvider>
</UrqlProvider>
</PageContextProvider>,
);
};

View File

@ -1,12 +1,14 @@
import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache } from '@apollo/client';
import { renderToStringWithData } from '@apollo/client/react/ssr';
import { createClient, fetchExchange, ssrExchange } from 'urql';
import { HelmetProvider, HelmetServerState } from 'react-helmet-async';
import { Provider as UrqlProvider } from 'urql';
import { dangerouslySkipEscape, escapeInject } from 'vike/server';
import type { OnRenderHtmlAsync } from 'vike/types';
import { App } from '../app';
import { typePolicies } from './policy';
import { renderToStringWithData } from './prepass';
import { PageProps } from './types';
import { PageContextProvider } from './usePageContext';
import { cacheExchange } from '@urql/exchange-graphcache';
import { cacheOptions } from './cache';
const GRAPHQL_URL = (import.meta.env.PUBLIC_ENV__FRONTEND_API_URL || import.meta.env.FRONTEND_API_URL) + '/graphql';
@ -15,30 +17,30 @@ export const onRenderHtml: OnRenderHtmlAsync = async (pageContext): ReturnType<O
const pageProps: PageProps = { routeParams: pageContext.routeParams };
const headers = pageContext.cookies ? { Cookie: pageContext.cookies } : undefined;
const client = new ApolloClient({
ssrMode: true,
cache: new InMemoryCache({ typePolicies }),
link: new HttpLink({
uri: GRAPHQL_URL,
credentials: 'same-origin',
const ssr = ssrExchange({ isClient: false });
const client = createClient({
url: GRAPHQL_URL,
exchanges: [ssr, cacheExchange(cacheOptions), fetchExchange],
fetchOptions: {
headers: headers,
}),
credentials: 'same-origin',
},
});
const helmetContext: { helmet?: HelmetServerState } = {};
const tree = (
<PageContextProvider pageContext={pageContext}>
<ApolloProvider client={client}>
<UrqlProvider value={client}>
<HelmetProvider context={helmetContext}>
<App>
<Page {...pageProps} />
</App>
</HelmetProvider>
</ApolloProvider>
</UrqlProvider>
</PageContextProvider>
);
const pageHtml = await renderToStringWithData(tree);
const pageHtml = await renderToStringWithData(client, tree);
const helmet = helmetContext.helmet!;
const documentHtml = escapeInject`<!DOCTYPE html>
<html lang="en">
@ -59,7 +61,7 @@ export const onRenderHtml: OnRenderHtmlAsync = async (pageContext): ReturnType<O
return {
documentHtml: documentHtml,
pageContext: {
state: client.cache.extract(),
state: ssr.extractData(),
},
};
};

View File

@ -0,0 +1,27 @@
import { CacheExchangeOpts } from '@urql/exchange-graphcache';
import { relayPagination } from '@urql/exchange-graphcache/extras';
export const cacheOptions: Partial<CacheExchangeOpts> = {
resolvers: {
User: {
files: relayPagination(),
pastes: relayPagination(),
},
},
keys: {
User: () => null,
Config: () => null,
ConfigHost: () => null,
FileMetadata: () => null,
ResourceLocations: () => null,
FilePage: () => null,
PastePage: () => null,
},
updates: {
Mutation: {
disableOTP: (result, args, cache) => {
cache.invalidate('Query', 'user');
},
},
},
};

View File

@ -1,13 +0,0 @@
import { TypePolicies } from '@apollo/client';
import { relayStylePagination } from '@apollo/client/utilities';
export const typePolicies: TypePolicies = {
Config: { keyFields: [] },
User: {
keyFields: [],
fields: {
files: relayStylePagination(),
pastes: relayStylePagination(),
},
},
};

View File

@ -0,0 +1,35 @@
import { VNode } from 'preact';
import renderToString from 'preact-render-to-string';
import { Client } from 'urql';
const MAX_DEPTH = 3;
const isPromiseLike = (value: unknown): value is Promise<unknown> => {
if (value && typeof (value as Promise<unknown>).then === 'function') return true;
return false;
};
/**
* Enables urql suspense, then re-renders the tree until there are no suspense errors.
* This is a hack workaround because both `react-ssr-prepass` and `preact-ssr-prepass` are not working, both have preact/react compat errors.
*/
export const renderToStringWithData = async (client: Client, tree: VNode, depth = 0): Promise<string> => {
try {
client.suspense = true;
const result = renderToString(tree);
client.suspense = false;
return result;
} catch (error) {
if (isPromiseLike(error)) {
if (depth > MAX_DEPTH) {
throw new Error(
`Exceeded max suspense depth. Try merge your queries so there are not ${MAX_DEPTH}+ on a single page.`,
);
}
await error;
return renderToStringWithData(client, tree, depth++);
}
throw error;
}
};

View File

@ -1,12 +1,12 @@
import { NormalizedCacheObject } from '@apollo/client';
import { FC } from 'react';
import { SSRData } from 'urql';
// https://vike.dev/pageContext#typescript
declare global {
namespace Vike {
interface PageContext {
Page: FC<PageProps>;
state?: NormalizedCacheObject;
state?: SSRData;
pageHtml?: string;
cookies?: string;
}

View File

@ -10,11 +10,6 @@
"skipLibCheck": true,
"esModuleInterop": true,
"composite": true,
"plugins": [
{
"name": "@0no-co/graphqlsp",
"schema": "../api/src/schema.gql"
}
]
}
"noUnusedLocals": true,
},
}

View File

@ -15,10 +15,12 @@ export default defineConfig({
},
],
optimizeDeps: {
include: ['preact', 'preact/devtools', 'preact/debug', 'preact/jsx-dev-runtime', 'preact/hooks'],
include: ['preact', 'preact/devtools', 'preact/debug', 'preact/jsx-dev-runtime', 'preact/hooks', 'urql'],
},
define: { 'process.env.NODE_ENV': '"production"' },
ssr: { noExternal: ['@apollo/client', 'prism-react-renderer', 'qrcode.react', 'formik', 'react-helmet-async'] },
ssr: {
noExternal: ['preact', 'urql', 'prism-react-renderer', 'qrcode.react', 'formik', 'react-helmet-async'],
},
plugins: [
preact(),
ssr({ disableAutoFullBuild: true }),

View File

@ -228,12 +228,6 @@ importers:
packages/web:
devDependencies:
'@0no-co/graphqlsp':
specifier: ^1.3.0
version: 1.3.0
'@apollo/client':
specifier: ^3.8.9
version: 3.8.10(@preact/compat@17.1.2)(@preact/compat@17.1.2)(graphql@16.8.1)
'@atlasbot/configs':
specifier: ^10.5.15
version: 10.5.15(typescript@5.3.3)
@ -276,6 +270,12 @@ importers:
'@types/react-dom':
specifier: ^18.2.18
version: 18.2.18
'@urql/devtools':
specifier: ^2.0.3
version: 2.0.3(@urql/core@4.2.3)(graphql@16.8.1)
'@urql/exchange-graphcache':
specifier: ^6.4.1
version: 6.4.1(graphql@16.8.1)
autoprefixer:
specifier: ^10.4.16
version: 10.4.17(postcss@8.4.33)
@ -363,6 +363,9 @@ importers:
typescript:
specifier: ^5.3.3
version: 5.3.3
urql:
specifier: ^4.0.6
version: 4.0.6(graphql@16.8.1)
vavite:
specifier: ^4.0.1
version: 4.0.2(vite@5.0.12)
@ -376,16 +379,17 @@ importers:
specifier: ^1.3.3
version: 1.3.3
packages/web/dist/server: {}
packages:
/@0no-co/graphqlsp@1.3.0:
resolution: {integrity: sha512-eL7ZAvAmncEgAIjVgGcyKsrpIOmJ4tQfWxTDVPKxiIgRdRA56K+qjW2HtGdxE/WlufnNpbwcizU9D2HddYg1Zg==}
/@0no-co/graphql.web@1.0.4(graphql@16.8.1):
resolution: {integrity: sha512-W3ezhHGfO0MS1PtGloaTpg0PbaT8aZSmmaerL7idtU5F7oCI+uu25k+MsMS31BVFlp4aMkHSrNRxiD72IlK8TA==}
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
peerDependenciesMeta:
graphql:
optional: true
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
graphql: 16.8.1
dev: true
/@aashutoshrathi/word-wrap@1.2.6:
@ -406,41 +410,6 @@ packages:
'@jridgewell/trace-mapping': 0.3.22
dev: true
/@apollo/client@3.8.10(@preact/compat@17.1.2)(@preact/compat@17.1.2)(graphql@16.8.1):
resolution: {integrity: sha512-p/22RZ8ehHyvySnC20EHPPe0gdu8Xp6ZCiXOfdEe1ZORw5cUteD/TLc66tfKv8qu8NLIfbiWoa+6s70XnKvxqg==}
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
graphql-ws: ^5.5.5
react: '*'
react-dom: '*'
subscriptions-transport-ws: ^0.9.0 || ^0.11.0
peerDependenciesMeta:
graphql-ws:
optional: true
react:
optional: true
react-dom:
optional: true
subscriptions-transport-ws:
optional: true
dependencies:
'@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1)
'@wry/equality': 0.5.7
'@wry/trie': 0.5.0
graphql: 16.8.1
graphql-tag: 2.12.6(graphql@16.8.1)
hoist-non-react-statics: 3.3.2
optimism: 0.18.0
prop-types: 15.8.1
react: /@preact/compat@17.1.2(preact@10.19.3)
react-dom: /@preact/compat@17.1.2(preact@10.19.3)
response-iterator: 0.2.6
symbol-observable: 4.0.0
ts-invariant: 0.10.3
tslib: 2.6.2
zen-observable-ts: 1.2.5
dev: true
/@ardatan/relay-compiler@12.0.0(graphql@16.8.1):
resolution: {integrity: sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==}
hasBin: true
@ -1434,7 +1403,7 @@ packages:
'@fastify/reply-from': 9.7.0
fast-querystring: 1.1.2
fastify-plugin: 4.5.1
ws: 8.16.0
ws: 8.16.0(utf-8-validate@6.0.3)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -1836,7 +1805,7 @@ packages:
graphql-ws: 5.14.3(graphql@16.8.1)
isomorphic-ws: 5.0.0(ws@8.16.0)
tslib: 2.6.2
ws: 8.16.0
ws: 8.16.0(utf-8-validate@6.0.3)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -1871,7 +1840,7 @@ packages:
graphql: 16.8.1
isomorphic-ws: 5.0.0(ws@8.16.0)
tslib: 2.6.2
ws: 8.16.0
ws: 8.16.0(utf-8-validate@6.0.3)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -2123,7 +2092,7 @@ packages:
isomorphic-ws: 5.0.0(ws@8.16.0)
tslib: 2.6.2
value-or-promise: 1.0.12
ws: 8.16.0
ws: 8.16.0(utf-8-validate@6.0.3)
transitivePeerDependencies:
- '@types/node'
- bufferutil
@ -4308,6 +4277,36 @@ packages:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true
/@urql/core@4.2.3(graphql@16.8.1):
resolution: {integrity: sha512-DJ9q9+lcs5JL8DcU2J3NqsgeXYJva+1+Qt8HU94kzTPqVOIRRA7ouvy4ksUfPY+B5G2PQ+vLh+JJGyZCNXv0cg==}
dependencies:
'@0no-co/graphql.web': 1.0.4(graphql@16.8.1)
wonka: 6.3.4
transitivePeerDependencies:
- graphql
dev: true
/@urql/devtools@2.0.3(@urql/core@4.2.3)(graphql@16.8.1):
resolution: {integrity: sha512-TktPLiBS9LcBPHD6qcnb8wqOVcg3Bx0iCtvQ80uPpfofwwBGJmqnQTjUdEFU6kwaLOFZULQ9+Uo4831G823mQw==}
peerDependencies:
'@urql/core': '>= 1.14.0'
graphql: '>= 0.11.0'
dependencies:
'@urql/core': 4.2.3(graphql@16.8.1)
graphql: 16.8.1
wonka: 6.3.4
dev: true
/@urql/exchange-graphcache@6.4.1(graphql@16.8.1):
resolution: {integrity: sha512-hWa4/5B7Op93oA6yWvffPU3L0XH55tlluEaq6aoFE9zsiNhktFjhp/SU2CKvSD8iD0Tfreoo63drv64Ad0+Gxw==}
dependencies:
'@0no-co/graphql.web': 1.0.4(graphql@16.8.1)
'@urql/core': 4.2.3(graphql@16.8.1)
wonka: 6.3.4
transitivePeerDependencies:
- graphql
dev: true
/@vavite/connect@4.0.2(vite@5.0.12):
resolution: {integrity: sha512-wEEjsKXUvmOEzQ5jJm33AAKmX9tNwjuxmTA5CHwHzria4QcjRUxgONo0jjhvIY4S+e9/gkf6e24rREjAGfgcfA==}
peerDependencies:
@ -4453,41 +4452,6 @@ packages:
tslib: 2.6.2
dev: true
/@wry/caches@1.0.1:
resolution: {integrity: sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==}
engines: {node: '>=8'}
dependencies:
tslib: 2.6.2
dev: true
/@wry/context@0.7.4:
resolution: {integrity: sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==}
engines: {node: '>=8'}
dependencies:
tslib: 2.6.2
dev: true
/@wry/equality@0.5.7:
resolution: {integrity: sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==}
engines: {node: '>=8'}
dependencies:
tslib: 2.6.2
dev: true
/@wry/trie@0.4.3:
resolution: {integrity: sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==}
engines: {node: '>=8'}
dependencies:
tslib: 2.6.2
dev: true
/@wry/trie@0.5.0:
resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==}
engines: {node: '>=8'}
dependencies:
tslib: 2.6.2
dev: true
/abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@ -7333,7 +7297,7 @@ packages:
peerDependencies:
ws: '*'
dependencies:
ws: 8.16.0
ws: 8.16.0(utf-8-validate@6.0.3)
dev: true
/istextorbinary@9.5.0:
@ -8476,7 +8440,6 @@ packages:
/node-gyp-build@4.8.0:
resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==}
hasBin: true
dev: false
/node-html-parser@6.1.12:
resolution: {integrity: sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==}
@ -8661,15 +8624,6 @@ packages:
mimic-fn: 4.0.0
dev: true
/optimism@0.18.0:
resolution: {integrity: sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ==}
dependencies:
'@wry/caches': 1.0.1
'@wry/context': 0.7.4
'@wry/trie': 0.4.3
tslib: 2.6.2
dev: true
/optionator@0.9.3:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
engines: {node: '>= 0.8.0'}
@ -9613,11 +9567,6 @@ packages:
supports-preserve-symlinks-flag: 1.0.0
dev: true
/response-iterator@0.2.6:
resolution: {integrity: sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==}
engines: {node: '>=0.8'}
dev: true
/restore-cursor@3.1.0:
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
engines: {node: '>=8'}
@ -10225,11 +10174,6 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/symbol-observable@4.0.0:
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
engines: {node: '>=0.10'}
dev: true
/syncpack@12.3.0(typescript@5.3.3):
resolution: {integrity: sha512-Gz2uGn96OmGfVVlKztvFac1EJYjP+WptQ2ohA6Uf48C6qLkhSayhkdujKQ6q7bGOTy8HSGI0iDfwfCJu6wvRig==}
engines: {node: '>=16'}
@ -10456,13 +10400,6 @@ packages:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
dev: true
/ts-invariant@0.10.3:
resolution: {integrity: sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==}
engines: {node: '>=8'}
dependencies:
tslib: 2.6.2
dev: true
/ts-log@2.2.5:
resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==}
dev: true
@ -10916,6 +10853,15 @@ packages:
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
dev: true
/urql@4.0.6(graphql@16.8.1):
resolution: {integrity: sha512-meXJ2puOd64uCGKh7Fse2R7gPa8+ZpBOoA62jN7CPXXUt7SVZSdeXWSpB3HvlfzLUkEqsWbvshwrgeWRYNNGaQ==}
dependencies:
'@urql/core': 4.2.3(graphql@16.8.1)
wonka: 6.3.4
transitivePeerDependencies:
- graphql
dev: true
/use-callback-ref@1.3.1(@types/react@18.2.48):
resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==}
engines: {node: '>=10'}
@ -10953,7 +10899,6 @@ packages:
requiresBuild: true
dependencies:
node-gyp-build: 4.8.0
dev: false
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -11291,6 +11236,10 @@ packages:
stackback: 0.0.2
dev: true
/wonka@6.3.4:
resolution: {integrity: sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg==}
dev: true
/wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
dev: true
@ -11355,19 +11304,6 @@ packages:
utf-8-validate: 6.0.3
dev: false
/ws@8.16.0:
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: true
/ws@8.16.0(utf-8-validate@6.0.3):
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
engines: {node: '>=10.0.0'}
@ -11381,7 +11317,6 @@ packages:
optional: true
dependencies:
utf-8-validate: 6.0.3
dev: false
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
@ -11478,16 +11413,6 @@ packages:
type-fest: 2.19.0
dev: true
/zen-observable-ts@1.2.5:
resolution: {integrity: sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==}
dependencies:
zen-observable: 0.8.15
dev: true
/zen-observable@0.8.15:
resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==}
dev: true
/zod-validation-error@2.1.0(zod@3.22.4):
resolution: {integrity: sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==}
engines: {node: '>=18.0.0'}