Skip to content

Commit

Permalink
GraphQL with react-query (#368)
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-lee authored Nov 25, 2024
1 parent 220484d commit 57006a5
Show file tree
Hide file tree
Showing 40 changed files with 821 additions and 1,371 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* text=auto
packages/zudoku/src/lib/plugins/openapi/graphql/** linguist-generated
packages/zudoku/schema.graphql linguist-generated
10 changes: 4 additions & 6 deletions packages/zudoku/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"./plugins/redirect": "./src/lib/plugins/redirect/index.ts",
"./plugins/custom-pages": "./src/lib/plugins/custom-pages/index.ts",
"./plugins/search-inkeep": "./src/lib/plugins/search-inkeep/index.ts",
"./openapi-worker": "./src/lib/plugins/openapi-worker.ts",
"./components": "./src/lib/components/index.ts",
"./icons": "./src/lib/icons.ts",
"./vite": "./src/vite/plugin.ts",
Expand Down Expand Up @@ -96,10 +95,6 @@
"import": "./lib/zudoku.plugin-search-inkeep.js",
"types": "./dist/lib/plugins/search-inkeep/index.d.ts"
},
"./openapi-worker": {
"import": "./lib/zudoku.openapi-worker.js",
"types": "./dist/lib/plugins/openapi-worker.d.ts"
},
"./components": {
"import": "./lib/zudoku.components.js",
"types": "./dist/lib/components/index.d.ts"
Expand Down Expand Up @@ -132,6 +127,7 @@
"build:standalone:html": "cp ./src/app/standalone.html ./standalone/standalone.html && cp ./src/app/demo.html ./standalone/demo.html && cp ./src/app/demo-cdn.html ./standalone/index.html",
"hack:fix-worker-paths": "node ./scripts/hack-worker.mjs",
"clean": "tsc --build --clean",
"codegen": "graphql-codegen --config ./src/codegen.ts",
"test": "node --test --enable-source-maps"
},
"dependencies": {
Expand Down Expand Up @@ -169,7 +165,7 @@
"@sindresorhus/slugify": "2.2.1",
"@stefanprobst/rehype-extract-toc": "2.2.0",
"@tailwindcss/typography": "0.5.15",
"@tanstack/react-query": "5.59.13",
"@tanstack/react-query": "5.61.0",
"@types/react": "18.3.11",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "4.3.1",
Expand Down Expand Up @@ -233,6 +229,8 @@
"zustand": "5.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.3",
"@graphql-codegen/client-preset": "4.5.0",
"@types/express": "5.0.0",
"@types/har-format": "1.2.15",
"@types/json-schema": "7.0.15",
Expand Down
3 changes: 1 addition & 2 deletions packages/zudoku/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
"build:standalone:html": {
"dependsOn": ["^build"],
"dependsOn": ["build:standalone:vite"]
},
"codegen": {}
}
}
}
57 changes: 30 additions & 27 deletions packages/zudoku/schema.graphql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 8 additions & 6 deletions packages/zudoku/src/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { dehydrate } from "@tanstack/react-query";
import { type HelmetData } from "@zudoku/react-helmet-async";
import type express from "express";
import logger from "loglevel";
Expand All @@ -11,7 +12,7 @@ import {
import "virtual:zudoku-theme.css";
import { BootstrapStatic, ServerError } from "zudoku/components";
import type { ZudokuConfig } from "../config/config.js";
import { ssr as urqlSsr } from "../lib/plugins/openapi/client/createMemoryClient.js";
import { queryClient } from "../lib/core/ZudokuContext.js";
import type { FileWritingResponse } from "../vite/prerender.js";
import "./main.css";
import { getRoutesByConfig } from "./main.js";
Expand Down Expand Up @@ -63,7 +64,6 @@ export const render = async ({

const router = createStaticRouter(dataRoutes, context);
const helmetContext = {} as HelmetData["context"];
const graphqlData = urqlSsr.extractData();

const { pipe } = renderToPipeableStream(
<BootstrapStatic
Expand Down Expand Up @@ -112,14 +112,16 @@ export const render = async ({
),
);

transformStream.on("finish", () => {
const reactQueryData = dehydrate(queryClient);

transformStream.on("finish", () =>
response.end(
htmlEnd?.replace(
"</body>",
`<script>window.__URQL_DATA__ = ${JSON.stringify(graphqlData)};</script></body>`,
`<script>window.DATA = ${JSON.stringify(reactQueryData)}</script></body>`,
),
);
});
),
);

pipe(transformStream);
},
Expand Down
32 changes: 24 additions & 8 deletions scripts/src/codegen.ts → packages/zudoku/src/codegen.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
import type { CodegenConfig } from "@graphql-codegen/cli";
import { printSchema } from "graphql";
import fs from "node:fs/promises";
import { schema } from "./lib/oas/graphql/index.js";

async function fixImports(filePath: string) {
const data = await fs.readFile(filePath, "utf8");

const result = data.replaceAll(
/(import|export) (.*) from (['"])(\.\/[^'"]*)\3/gm,
// Handle imports/exports
let result = data.replace(
/(import|export) (.*) from (['"])(\.\/[^'"]*)\3/g,
(match, keyword, p1, quote, p2) =>
p2.endsWith(".js")
? match
: `${keyword} ${p1} from ${quote}${p2}.js${quote}`,
);

// Handle dynamic imports
result = result.replace(
/import\((['"])(\.\/[^'"]*)\1\)/g,
(match, quote, p2) =>
p2.endsWith(".js") ? match : `import(${quote}${p2}.js${quote})`,
);

await fs.writeFile(filePath, result, "utf8");
}
const zudokuPkgDir = new URL("../../packages/zudoku", import.meta.url).pathname;

const config: CodegenConfig = {
overwrite: true,
schema: "http://localhost:9000/__z/graphql",
documents: `${zudokuPkgDir}/src/lib/plugins/openapi/**/*.tsx`,
schema: printSchema(schema),
documents: ["src/lib/plugins/openapi/**/*.tsx"],
generates: {
[`${zudokuPkgDir}/src/lib/plugins/openapi/graphql/`]: {
["./src/lib/plugins/openapi/graphql/"]: {
preset: "client",
plugins: [],
config: {
documentMode: "string",
},
},
"schema.graphql": {
plugins: ["schema-ast"],
config: {
includeDirectives: true,
},
},
},
config: {
Expand Down
7 changes: 6 additions & 1 deletion packages/zudoku/src/lib/components/DeveloperHint.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import { Callout } from "../ui/Callout.js";
import { Markdown } from "./Markdown.js";

export const DeveloperHint = ({
children,
Expand All @@ -13,7 +14,11 @@ export const DeveloperHint = ({
return (
<Callout type="caution" title="Developer hint" className={className}>
<div className="flex flex-col gap-2">
<div>{children}</div>
{typeof children === "string" ? (
<Markdown content={children} />
) : (
<div>{children}</div>
)}
<small className="italic">
Note: This hint is only shown in development mode.
</small>
Expand Down
6 changes: 3 additions & 3 deletions packages/zudoku/src/lib/components/TopNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { cx } from "class-variance-authority";
import { Suspense } from "react";
import { Link } from "react-router-dom";
import { useAuth } from "../authentication/hook.js";
import { TopNavigationItem } from "../../config/validators/validate.js";
import { useAuth } from "../authentication/hook.js";
import { joinPath } from "../util/joinPath.js";
import { useCurrentNavigation, useZudoku } from "./context/ZudokuContext.js";
import { traverseSidebar } from "./navigation/utils.js";
Expand Down Expand Up @@ -32,7 +32,7 @@ export const TopNavigation = () => {
<nav className="hidden lg:block border-b text-sm px-12 h-[--top-nav-height]">
<ul className="flex flex-row items-center gap-8">
{topNavigation.filter(isHiddenItem(isAuthenticated)).map((item) => (
<li key={item.id}>
<li key={item.id}>
<TopNavItem {...item} />
</li>
))}
Expand Down Expand Up @@ -66,7 +66,7 @@ const TopNavItem = ({ id, label, default: defaultLink }: TopNavigationItem) => {
}

// Manually set the active sidebar based on our logic of what is active
const isActive = nav.data.topNavItem?.id === id;
const isActive = nav.topNavItem?.id === id;

return (
<Link
Expand Down
40 changes: 23 additions & 17 deletions packages/zudoku/src/lib/components/Zudoku.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MDXProvider } from "@mdx-js/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { HydrationBoundary, QueryClientProvider } from "@tanstack/react-query";
import { Helmet } from "@zudoku/react-helmet-async";
import { ThemeProvider } from "next-themes";
import {
Expand Down Expand Up @@ -78,22 +78,28 @@ const ZudokoInner = memo(

return (
<QueryClientProvider client={queryClient}>
<Helmet>{heads}</Helmet>
<StaggeredRenderContext.Provider value={staggeredValue}>
<ZudokuProvider context={zudokuContext}>
<MDXProvider components={mdxComponents}>
<ThemeProvider attribute="class" disableTransitionOnChange>
<ComponentsProvider value={components}>
<SlotletProvider slotlets={props.slotlets}>
<ViewportAnchorProvider>
{children ?? <Outlet />}
</ViewportAnchorProvider>
</SlotletProvider>
</ComponentsProvider>
</ThemeProvider>
</MDXProvider>
</ZudokuProvider>
</StaggeredRenderContext.Provider>
<HydrationBoundary
state={
typeof window !== "undefined" ? (window as any).DATA : undefined
}
>
<Helmet>{heads}</Helmet>
<StaggeredRenderContext.Provider value={staggeredValue}>
<ZudokuProvider context={zudokuContext}>
<MDXProvider components={mdxComponents}>
<ThemeProvider attribute="class" disableTransitionOnChange>
<ComponentsProvider value={components}>
<SlotletProvider slotlets={props.slotlets}>
<ViewportAnchorProvider>
{children ?? <Outlet />}
</ViewportAnchorProvider>
</SlotletProvider>
</ComponentsProvider>
</ThemeProvider>
</MDXProvider>
</ZudokuProvider>
</StaggeredRenderContext.Provider>
</HydrationBoundary>
</QueryClientProvider>
);
},
Expand Down
21 changes: 8 additions & 13 deletions packages/zudoku/src/lib/components/context/ZudokuContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,13 @@ export const useCurrentNavigation = () => {
topNavigation.find((t) => t.id === currentSidebarItem?.[0]) ??
topNavigation.find((item) => matchPath(item.id, location.pathname));

return useSuspenseQuery({
queryFn: async () => {
const pluginSidebar = await getPluginSidebar(location.pathname);

return {
sidebar: [
...(currentSidebarItem ? currentSidebarItem[1] : []),
...pluginSidebar,
],
topNavItem: currentTopNavItem,
};
},
queryKey: ["navigation", location.pathname],
const { data } = useSuspenseQuery({
queryFn: () => getPluginSidebar(location.pathname),
queryKey: ["plugin-sidebar", location.pathname],
});

return {
sidebar: [...(currentSidebarItem ? currentSidebarItem[1] : []), ...data],
topNavItem: currentTopNavItem,
};
};
Loading

0 comments on commit 57006a5

Please sign in to comment.