diff --git a/app/components/ArticlesList.tsx b/app/components/ArticlesList.tsx index 63d2bc4..56f97da 100644 --- a/app/components/ArticlesList.tsx +++ b/app/components/ArticlesList.tsx @@ -3,7 +3,7 @@ import { Link } from "remix"; import type { ArticleItemFragment } from "~/graphql/generated"; import { formatRelative, parseISO } from "date-fns"; import { getDateFnsLocale } from "~/utils/i18n"; -import { gql } from "~/utils/dato"; +import { gql } from "@urql/core"; export const itemFragment = gql` fragment articleItem on ArticleRecord { diff --git a/app/components/GNB.tsx b/app/components/GNB.tsx index 857632c..3e834e5 100644 --- a/app/components/GNB.tsx +++ b/app/components/GNB.tsx @@ -2,7 +2,7 @@ import { Globe } from "react-feather"; import { Link } from "remix"; import invariant from "tiny-invariant"; import { GnbFragmentFragment } from "~/graphql/generated"; -import { gql } from "~/utils/dato"; +import { gql } from "@urql/core"; interface GNBProps { data: GnbFragmentFragment | null; diff --git a/app/components/Image.tsx b/app/components/Image.tsx index 3aeeb1b..79e6f5c 100644 --- a/app/components/Image.tsx +++ b/app/components/Image.tsx @@ -1,6 +1,6 @@ import invariant from "tiny-invariant"; import { ImageFragment } from "~/graphql/generated"; -import { gql } from "~/utils/dato"; +import { gql } from "@urql/core"; export const fragment = gql` fragment Image on ImageRecord { diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 9e4add6..c72f536 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -1,11 +1,13 @@ import { renderToPipeableStream } from "react-dom/server"; import { createCookie, RemixServer } from "remix"; import type { EntryContext } from "remix"; -import { gql, load } from "./utils/dato"; +import { load } from "./utils/dato"; import { GetLocalesQuery } from "./graphql/generated"; import { PassThrough } from "stream"; +import { gql } from "@urql/core"; import "dotenv/config"; +import invariant from "tiny-invariant"; let locales: string[]; @@ -23,7 +25,7 @@ export default async function handleRequest( }); if (!locales) { - const data: GetLocalesQuery = await load({ + const { data, error } = await load({ query: gql` query getLocales { _site { @@ -32,6 +34,8 @@ export default async function handleRequest( } `, }); + if (error) throw error; + invariant(data, "data is undefined"); locales = data._site.locales; } diff --git a/app/root.tsx b/app/root.tsx index 60bf875..8dd1a66 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -19,7 +19,8 @@ import css from "./theme.css"; import logo from "~/assets/basixlab.svg"; import GNB, { fragment as gnbFragment } from "./components/GNB"; -import { datoQuerySubscription, gql, QueryListenerOptions } from "./utils/dato"; +import { datoQuerySubscription, QueryListenerOptions } from "./utils/dato"; +import { gql } from "@urql/core"; import { RootQuery } from "./graphql/generated"; import { renderMetaTags, useQuerySubscription } from "react-datocms"; import { MetaTagsFragment } from "./graphql/fragments"; diff --git a/app/routes/$locale/index.tsx b/app/routes/$locale/index.tsx index 2550206..645a143 100644 --- a/app/routes/$locale/index.tsx +++ b/app/routes/$locale/index.tsx @@ -5,7 +5,8 @@ import ArticlesList from "~/components/ArticlesList"; import { StructuredText } from "~/components/dato"; import { HomepageQuery } from "~/graphql/generated"; import { OutletData } from "~/root"; -import { datoQuerySubscription, gql, QueryListenerOptions } from "~/utils/dato"; +import { datoQuerySubscription, QueryListenerOptions } from "~/utils/dato"; +import { gql } from "@urql/core"; import { fragment as articlesListFragment } from "~/components/ArticlesList"; import { fragment as imageFragment } from "~/components/Image"; diff --git a/app/routes/$locale/posts/$id.tsx b/app/routes/$locale/posts/$id.tsx index 0e60531..96a78e6 100644 --- a/app/routes/$locale/posts/$id.tsx +++ b/app/routes/$locale/posts/$id.tsx @@ -5,7 +5,8 @@ import { useOutletContext, } from "remix"; import { Giscus } from "@giscus/react"; -import { datoQuerySubscription, gql, QueryListenerOptions } from "~/utils/dato"; +import { datoQuerySubscription, QueryListenerOptions } from "~/utils/dato"; +import { gql } from "@urql/core"; import type { ArticlesListRecord, GetPostQuery } from "~/graphql/generated"; import { toRemixMeta, useQuerySubscription } from "react-datocms"; import { MetaTagsFragment } from "~/graphql/fragments"; @@ -83,10 +84,7 @@ export default function Post() {

{data?.article?.description}

- + {data?.article?.comments && ( > { + query: string | DocumentNode | TypedDocumentNode; + variables?: V; preview?: boolean; } -export async function load({ +export async function load>({ query, variables, preview, -}: LoadOptions): Promise { - let endpoint = "https://graphql.datocms.com"; +}: LoadOptions): Promise> { + let endpoint = ENDPOINT; if (process.env.DATOCMS_ENVIRONMENT) endpoint += `/environments/${process.env.DATOCMS_ENVIRONMENT}`; if (preview) endpoint += `/preview`; - const headers = new Headers({ - Authorization: `Bearer ${process.env.DATOCMS_READONLY_TOKEN}`, - }); - - const res = await fetch(endpoint, { - method: "POST", - headers, - body: JSON.stringify({ - query, - variables, - }), - }); + const client = endpoint === ENDPOINT ? defaultClient : getClient(endpoint); - const json = await res.json(); - - if (!res.ok || json.errors) { - console.error("Ouch! The query has some errors!", JSON.stringify(json.errors, null, 2)); - throw new Error(json.errors ?? json); - } - - return json.data; + return await client.query(query, variables).toPromise(); } function getEnvironment() { if (process.env.DATOCMS_ENVIRONMENT) return process.env.DATOCMS_ENVIRONMENT; else if (process.env.NODE_ENV === "development") return "development"; - else if (process.env.VERCEL_GIT_COMMIT_REF?.startsWith("env/")) return process.env.VERCEL_GIT_COMMIT_REF.slice(4); + else if (process.env.VERCEL_GIT_COMMIT_REF?.startsWith("env/")) + return process.env.VERCEL_GIT_COMMIT_REF.slice(4); } -interface QueryOptions extends LoadOptions { +interface QueryOptions extends LoadOptions { request: Request; } -export async function datoQuerySubscription({ +export async function datoQuerySubscription< + T = any, + V extends object = Record +>({ request, + query, ...gqlRequest -}: QueryOptions): Promise>> { +}: QueryOptions): Promise< + DatoQueryListenerOptions +> { const session = await getSession(request.headers.get("Cookie")); const previewEnabled = session.get("preview"); + const { data, error } = await load({ + ...gqlRequest, + query, + preview: Boolean(previewEnabled), + }); + + const stringQuery = typeof query !== 'string' ? print(query) : query; + + if (error) throw error; + return previewEnabled ? { ...gqlRequest, + query: stringQuery, preview: true, - initialData: await load({ ...gqlRequest, preview: true }), + initialData: data, token: process.env.DATOCMS_READONLY_TOKEN!, environment: getEnvironment(), } : { enabled: false, - initialData: await load(gqlRequest), + initialData: data, }; } -export type QueryListenerOptions = DatoQueryListenerOptions>; - -// Trick for better editor support. -export const gql = dedent; +export type QueryListenerOptions = DatoQueryListenerOptions< + T, + Record +>; diff --git a/app/utils/feed.ts b/app/utils/feed.ts index 25bd164..5dbbb9a 100644 --- a/app/utils/feed.ts +++ b/app/utils/feed.ts @@ -1,6 +1,7 @@ import { Feed } from "feed"; import { FeedQuery } from "~/graphql/generated"; -import { gql, load } from "./dato"; +import { load } from "./dato"; +import { gql } from "@urql/core"; import { render } from "datocms-structured-text-to-plain-text"; export async function getFeed(url: URL, language: string): Promise { diff --git a/app/utils/graphql.server.ts b/app/utils/graphql.server.ts new file mode 100644 index 0000000..10642d8 --- /dev/null +++ b/app/utils/graphql.server.ts @@ -0,0 +1,23 @@ +import { createClient, ClientOptions } from "@urql/core"; + +const clientOptions: Omit = { + fetchOptions: { + headers: { + Authorization: `Bearer ${process.env.DATOCMS_READONLY_TOKEN}`, + }, + }, +}; + +export const ENDPOINT = "https://graphql.datocms.com"; + +export const defaultClient = createClient({ + url: ENDPOINT, + ...clientOptions, +}); + +export function getClient(url: string) { + return createClient({ + url, + ...clientOptions, + }); +} diff --git a/codegen.yml b/codegen.yml index a95ed8d..58f6e55 100644 --- a/codegen.yml +++ b/codegen.yml @@ -32,4 +32,4 @@ generates: - typescript-document-nodes config: importOperationTypesFrom: ./generated - gqlImport: ~/utils/dato#gql + gqlImport: "@urql/core#gql" diff --git a/package.json b/package.json index 79f83c7..dcb6059 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "@remix-run/react": "^1.1.3", "@remix-run/serve": "^1.1.3", "@remix-run/vercel": "patch:@remix-run/vercel@npm:1.1.3#.yarn/patches/@remix-run-vercel-npm-1.1.3-3d4b846e7d", + "@urql/core": "^2.4.1", + "@urql/exchange-graphcache": "^4.3.6", "date-fns": "^2.28.0", "datocms-structured-text-to-plain-text": "^2.0.0", "dedent": "^0.7.0", @@ -29,7 +31,8 @@ "remix": "^1.1.3", "remix-crash": "^0.1.2", "rosetta": "^1.1.0", - "tiny-invariant": "^1.2.0" + "tiny-invariant": "^1.2.0", + "urql": "^2.1.3" }, "devDependencies": { "@graphql-codegen/cli": "^2.5.0", diff --git a/yarn.lock b/yarn.lock index 921327b..6378b4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1125,6 +1125,15 @@ __metadata: languageName: node linkType: hard +"@graphql-typed-document-node/core@npm:^3.1.1": + version: 3.1.1 + resolution: "@graphql-typed-document-node/core@npm:3.1.1" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 87ff4cee308f1075f4472b80f9f9409667979940f8f701e87f0aa35ce5cf104d94b41258ea8192d05a0893475cd0f086a3929a07663b4fe8d0e805a277f07ed5 + languageName: node + linkType: hard + "@iarna/toml@npm:^2.2.5": version: 2.2.5 resolution: "@iarna/toml@npm:2.2.5" @@ -1687,6 +1696,30 @@ __metadata: languageName: node linkType: hard +"@urql/core@npm:>=2.3.6, @urql/core@npm:^2.4.1": + version: 2.4.1 + resolution: "@urql/core@npm:2.4.1" + dependencies: + "@graphql-typed-document-node/core": ^3.1.1 + wonka: ^4.0.14 + peerDependencies: + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: e4858ae1062639c7a3c9357ddaa745e4757ae708a914630a81f393a8bdf6c37cf97f804f41bf2216686f09ddc8d529017e03cc572b445e6cff50406ca55eda55 + languageName: node + linkType: hard + +"@urql/exchange-graphcache@npm:^4.3.6": + version: 4.3.6 + resolution: "@urql/exchange-graphcache@npm:4.3.6" + dependencies: + "@urql/core": ">=2.3.6" + wonka: ^4.0.14 + peerDependencies: + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 861d52572d14d3a0a4c15068d6680b3b0ee4ec091ab5f16fe3b43e476cb14d628748bce3a2b1c81fdc46c407ef3a42883f9859e424da432ab57dbf34bd0ae7ec + languageName: node + linkType: hard + "@web-std/blob@npm:^3.0.3": version: 3.0.3 resolution: "@web-std/blob@npm:3.0.3" @@ -7367,6 +7400,8 @@ __metadata: "@types/react": ^17.0.24 "@types/react-dom": ^17.0.9 "@types/react-syntax-highlighter": ^13.5.2 + "@urql/core": ^2.4.1 + "@urql/exchange-graphcache": ^4.3.6 date-fns: ^2.28.0 datocms-structured-text-to-plain-text: ^2.0.0 dedent: ^0.7.0 @@ -7385,6 +7420,7 @@ __metadata: rosetta: ^1.1.0 tiny-invariant: ^1.2.0 typescript: ^4.1.2 + urql: ^2.1.3 languageName: unknown linkType: soft @@ -8588,6 +8624,20 @@ __metadata: languageName: node linkType: hard +"urql@npm:^2.1.3": + version: 2.1.3 + resolution: "urql@npm:2.1.3" + dependencies: + "@urql/core": ^2.4.1 + use-sync-external-store: 1.0.0-rc.0 || ^1.0.0 + wonka: ^4.0.14 + peerDependencies: + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + react: ">= 16.8.0" + checksum: 78bdc87d5dac035a282600a5787ce7c1024413155cb52d400fb192371d66a4135ca64f6fe490b5611b47ae1f52914eefd52ae2f283a3589d8cb000222a421f47 + languageName: node + linkType: hard + "use-deep-compare-effect@npm:^1.6.1": version: 1.8.1 resolution: "use-deep-compare-effect@npm:1.8.1" @@ -8600,6 +8650,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:1.0.0-rc.0 || ^1.0.0": + version: 1.0.0-rc.0 + resolution: "use-sync-external-store@npm:1.0.0-rc.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0-rc + checksum: 02d065226538f471ad5924fe6adf486ee2d5ad7167da6ecf0844e2eaff44ee4855ba4fa1a2ef0132b1114bd40098452fc692f19d7a2fa0c52766147f0006ab24 + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -8816,6 +8875,13 @@ __metadata: languageName: node linkType: hard +"wonka@npm:^4.0.14": + version: 4.0.15 + resolution: "wonka@npm:4.0.15" + checksum: afbee7359ed2d0a9146bf682f3953cb093f47d5f827e767e6ef33cb70ca6f30631afe5fe31dbb8d6c7eaed26c4ac6426e7c13568917c017ef6f42c71139b38f7 + languageName: node + linkType: hard + "wrap-ansi@npm:^3.0.1": version: 3.0.1 resolution: "wrap-ansi@npm:3.0.1"