Skip to content

Commit

Permalink
feat: launch darkly markdown component (#1550)
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored Sep 27, 2024
1 parent 272ee0c commit cb53af9
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 7 deletions.
1 change: 1 addition & 0 deletions packages/ui/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"jotai-effect": "^1.0.0",
"jotai-location": "^0.5.5",
"jsonpath": "^1.1.1",
"launchdarkly-js-client-sdk": "^3.4.0",
"lodash-es": "^4.17.21",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-mdx-jsx": "^3.1.0",
Expand Down
8 changes: 7 additions & 1 deletion packages/ui/app/src/hooks/useApiRoute.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useAtomValue } from "jotai";
import { Getter, useAtomValue } from "jotai";
import urlJoin from "url-join";
import { BASEPATH_ATOM, TRAILING_SLASH_ATOM } from "../atoms";

Expand Down Expand Up @@ -26,3 +26,9 @@ export function useApiRoute(route: FernDocsApiRoute, options?: Options): string
const includeTrailingSlash = useAtomValue(TRAILING_SLASH_ATOM);
return getApiRouteSupplier({ includeTrailingSlash, basepath, ...options })(route);
}

export function selectApiRoute(get: Getter, route: FernDocsApiRoute, options?: Options): string {
const basepath = get(BASEPATH_ATOM);
const includeTrailingSlash = get(TRAILING_SLASH_ATOM);
return getApiRouteSupplier({ includeTrailingSlash, basepath, ...options })(route);
}
4 changes: 4 additions & 0 deletions packages/ui/app/src/mdx/components/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RemoteFontAwesomeIcon } from "@fern-ui/components";
import type { MDXComponents } from "mdx/types";
import dynamic from "next/dynamic";
import { ComponentProps, PropsWithChildren, ReactElement } from "react";
import { FernErrorBoundaryProps, FernErrorTag } from "../../components/FernErrorBoundary";
import { AccordionGroup } from "./accordion";
Expand Down Expand Up @@ -34,6 +35,8 @@ import { Step, StepGroup } from "./steps";
import { TabGroup } from "./tabs";
import { Tooltip } from "./tooltip";

const LaunchDarkly = dynamic(() => import("./launchdarkly/LaunchDarkly").then((mod) => mod.LaunchDarkly));

const FERN_COMPONENTS = {
AccordionGroup,
Availability,
Expand All @@ -53,6 +56,7 @@ const FERN_COMPONENTS = {
EndpointResponseSnippet,
Frame,
Icon: RemoteFontAwesomeIcon,
LaunchDarkly,
ParamField,
Step,
StepGroup,
Expand Down
82 changes: 82 additions & 0 deletions packages/ui/app/src/mdx/components/launchdarkly/LaunchDarkly.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { captureException } from "@sentry/nextjs";
import { atom, useAtomValue } from "jotai";
import { loadable } from "jotai/utils";
import * as LDClient from "launchdarkly-js-client-sdk";
import { PropsWithChildren, ReactNode, useCallback, useEffect, useState } from "react";
import { selectApiRoute } from "../../../hooks/useApiRoute";

async function fetchClientSideId(route: string): Promise<string | undefined> {
const json = await fetch(route).then((res) => res.json());
return json?.["client-side-id"];
}

// this is a singleton atom that initializes the LaunchDarkly client-side SDK
const ldClientAtom = atom<Promise<LDClient.LDClient | undefined>>(async (get) => {
if (typeof window === "undefined") {
return undefined;
}
const route = selectApiRoute(get, "/api/fern-docs/integrations/launchdarkly");
const clientSideId = await fetchClientSideId(route);
if (!clientSideId) {
return undefined;
}

const client = LDClient.initialize(clientSideId, {
kind: "user",
anonymous: true,
});

await client.waitForInitialization();

return client;
});

const useLaunchDarklyFlag = (flag: string): boolean => {
const loadableClient = useAtomValue(loadable(ldClientAtom));
const client = loadableClient.state === "hasData" ? loadableClient.data : undefined;
if (loadableClient.state === "hasError") {
captureException(loadableClient.error);
}

const getFlagEnabled = useCallback(() => {
return client?.variation(flag, false) ?? false;
}, [client, flag]);

const [enabled, setEnabled] = useState(getFlagEnabled);

useEffect(() => {
setEnabled(getFlagEnabled());

if (!client) {
return;
}

const listener = () => {
setEnabled(getFlagEnabled());
};

client.on("ready", listener);
client.on("change", listener);

return () => {
client.off("ready", listener);
client.off("change", listener);
};
}, [client, flag, getFlagEnabled]);

return enabled;
};

export interface LaunchDarklyProps {
flag: string;
}

export function LaunchDarkly({ flag, children }: PropsWithChildren<LaunchDarklyProps>): ReactNode {
const ldClient = useLaunchDarklyFlag(flag);

if (!ldClient) {
return null;
}

return children;
}
1 change: 1 addition & 0 deletions packages/ui/app/src/mdx/components/launchdarkly/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./LaunchDarkly";
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getXFernHostEdge } from "@/server/xfernhost/edge";
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

export const runtime = "edge";

const LaunchDarklyEdgeConfigSchema = z.object({
// NOTE: this is client-side visible, so we should be careful about what we expose here if we add more fields
"client-side-id": z.string().optional(),
});

type LaunchDarklyEdgeConfigSchema = z.infer<typeof LaunchDarklyEdgeConfigSchema>;

export default async function handler(req: NextRequest): Promise<NextResponse<LaunchDarklyEdgeConfigSchema>> {
const domain = getXFernHostEdge(req);
try {
const config = (await get<Record<string, LaunchDarklyEdgeConfigSchema>>("launchdarkly"))?.[domain];
if (config == null) {
return NextResponse.json({}, { status: 404 });
}

return NextResponse.json(config);
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
return NextResponse.json({}, { status: 500 });
}
}
119 changes: 113 additions & 6 deletions pnpm-lock.yaml

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

7 changes: 7 additions & 0 deletions tests/launchdarkly/fern/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
instances:
- url: https://launchdarkly-test.docs.dev.buildwithfern.com
title: Launch Darkly Test
navigation: []
landing-page:
page: Test
path: test.mdx
4 changes: 4 additions & 0 deletions tests/launchdarkly/fern/fern.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"organization": "fern",
"version": "0.43.4-7-gfe56df403b"
}
13 changes: 13 additions & 0 deletions tests/launchdarkly/fern/test.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### Testing

<LaunchDarkly flag="flag-1">

Testing **flag 1**

</LaunchDarkly>

<LaunchDarkly flag="flag-2">

Testing **flag 2**

</LaunchDarkly>

0 comments on commit cb53af9

Please sign in to comment.