Skip to content

Commit 65ee9d7

Browse files
authored
chore: CDK setup (#2639)
1 parent 582ce6f commit 65ee9d7

File tree

12 files changed

+240
-15
lines changed

12 files changed

+240
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Deploy Local Preview (DEV)
2+
3+
on:
4+
push:
5+
branches:
6+
- app
7+
release:
8+
types: [created]
9+
tags:
10+
- "docs@*"
11+
12+
env:
13+
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
14+
TURBO_TEAM: "buildwithfern"
15+
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
16+
GITHUB_TOKEN: ${{ secrets.FERN_GITHUB_TOKEN }}
17+
18+
jobs:
19+
dev:
20+
if: github.event_name == 'push'
21+
runs-on: macos-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
- uses: aws-actions/configure-aws-credentials@v4
25+
with:
26+
aws-region: us-east-1
27+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
28+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
29+
- name: Install
30+
uses: ./.github/actions/install
31+
- name: Compile
32+
run: pnpm compile
33+
- name: Build local preview bundle
34+
run: pnpm --filter=@fern-docs/bundle run docs:build:local
35+
- name: Copy static and public files
36+
working-directory: packages/fern-docs/bundle
37+
run: cp -r .next/static .next/standalone/packages/fern-docs/bundle/.next && cp -r public .next/standalone/packages/fern-docs/bundle
38+
- name: Synthesize local preview bundle
39+
run: pnpm --filter=@fern-platform/cdk run deploy-app:dev2
40+
41+
prod:
42+
if: github.event_name == 'release' && startsWith(github.event.release.tag_name, 'docs@')
43+
runs-on: macos-latest
44+
steps:
45+
- uses: actions/checkout@v4
46+
- uses: aws-actions/configure-aws-credentials@v4
47+
with:
48+
aws-region: us-east-1
49+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
50+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
51+
- name: Install
52+
uses: ./.github/actions/install
53+
- name: Compile
54+
run: pnpm compile
55+
- name: Build local preview bundle
56+
run: pnpm --filter=@fern-docs/bundle run docs:build:local
57+
- name: Copy static and public files
58+
working-directory: packages/fern-docs/bundle
59+
run: cp -r .next/static .next/standalone/packages/fern-docs/bundle/.next && cp -r public .next/standalone/packages/fern-docs/bundle
60+
- name: Synthesize local preview bundle
61+
run: pnpm --filter=@fern-platform/cdk run deploy-app:prod

packages/cdk/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
],
1818
"scripts": {
1919
"cdk": "cdk",
20+
"deploy-app:dev2": "cdk deploy local-preview-bundle3-dev2 --require-approval never --progress events",
21+
"deploy-app:prod": "cdk deploy local-preview-bundle3-prod --require-approval never --progress events",
2022
"deploy:dev2": "cdk deploy local-preview-bundle2-dev2 --require-approval never --progress events",
2123
"deploy:prod": "cdk deploy local-preview-bundle2-prod --require-approval never --progress events",
24+
"synth-app:dev2": "cdk synth local-preview-bundle3-dev2",
2225
"synth:dev2": "cdk synth local-preview-bundle2-dev2"
2326
},
2427
"dependencies": {

packages/cdk/src/cdk.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async function main() {
2424
case EnvironmentType.Prod:
2525
new DocsFeStack(
2626
app,
27-
`local-preview-bundle2-${environmentType.toLowerCase()}`,
27+
`local-preview-bundle3-${environmentType.toLowerCase()}`,
2828
environmentType,
2929
{
3030
env: { account: "985111089818", region: "us-east-1" },
@@ -43,9 +43,10 @@ async function getEnvironments(): Promise<Environments> {
4343
{
4444
method: "GET",
4545
headers: {
46-
Authorization: "Bearer " + process.env["GITHUB_TOKEN"],
46+
/* eslint-disable turbo/no-undeclared-env-vars */
47+
Authorization: "Bearer " + process.env.GITHUB_TOKEN,
4748
},
4849
}
4950
);
50-
return (await response.json()) as any as Environments;
51+
return (await response.json()) as Environments;
5152
}

packages/cdk/src/docs-fe-stack.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { EnvironmentType } from "@fern-fern/fern-cloud-sdk/api";
1111

1212
const LOCAL_PREVIEW_BUNDLE_OUT_DIR = path.resolve(
1313
__dirname,
14-
"../../fern-docs/local-preview-bundle/out"
14+
"../../fern-docs/bundle/.next"
1515
);
1616

1717
export class DocsFeStack extends Stack {
@@ -22,8 +22,8 @@ export class DocsFeStack extends Stack {
2222
props?: StackProps
2323
) {
2424
super(scope, id, props);
25-
const bucket = new Bucket(this, "local-preview-bundle2", {
26-
bucketName: `${environmentType.toLowerCase()}-local-preview-bundle2`,
25+
const bucket = new Bucket(this, "local-preview-bundle3", {
26+
bucketName: `${environmentType.toLowerCase()}-local-preview-bundle3`,
2727
removalPolicy: RemovalPolicy.RETAIN,
2828
cors: [
2929
{
@@ -51,7 +51,7 @@ export class DocsFeStack extends Stack {
5151

5252
const local_preview_bundle_dist_zip = path.resolve(
5353
__dirname,
54-
"../../fern-docs/local-preview-bundle/dist/out.zip"
54+
"../../fern-docs/bundle/next.zip"
5555
);
5656
if (
5757
!fs.existsSync(LOCAL_PREVIEW_BUNDLE_OUT_DIR) ||
@@ -66,7 +66,7 @@ export class DocsFeStack extends Stack {
6666
LOCAL_PREVIEW_BUNDLE_OUT_DIR,
6767
local_preview_bundle_dist_zip
6868
).then(() => {
69-
new BucketDeployment(this, "deploy-local-preview-bundle2", {
69+
new BucketDeployment(this, "deploy-local-preview-bundle3", {
7070
sources: [Source.asset(local_preview_bundle_dist_zip)],
7171
destinationBucket: bucket,
7272
extract: false,
@@ -90,7 +90,7 @@ async function zipFolder(sourceFolder: string, zipFilePath: string) {
9090
const archive = archiver("zip");
9191

9292
archive.on("error", (err: unknown) => {
93-
reject(err);
93+
reject(err instanceof Error ? err : new Error(String(err)));
9494
});
9595

9696
output.on("close", function () {

packages/cdk/tsconfig.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{
22
"extends": "@fern-platform/configs/tsconfig/cli.json",
3-
"compilerOptions": { "outDir": "./dist", "rootDir": "./src" },
3+
"compilerOptions": {
4+
"outDir": "./dist",
5+
"rootDir": "./src",
6+
"module": "NodeNext",
7+
"moduleResolution": "NodeNext"
8+
},
49
"include": ["./src/**/*"],
510
"exclude": ["cdk.out"]
611
}

packages/fern-docs/bundle/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
"docs:build:local": "NEXT_PUBLIC_IS_LOCAL=1 next build && pnpm tsc --project tsconfig.local.json",
2222
"docs:dev": "next dev",
2323
"docs:dev:inspect": "NODE_OPTIONS='--inspect' next dev",
24+
"docs:hide-env": "mv .env.local .env.local.hidden",
2425
"docs:lint": "next lint",
26+
"docs:show-env": "mv .env.local.hidden .env.local",
2527
"docs:start": "next start",
2628
"docs:start:local": "pnpm run docs:build:local && NODE_ENV=production node dist/server.js",
2729
"format": "prettier --write --ignore-unknown \"**\"",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { revalidateTag } from "next/cache";
2+
import { NextRequest, NextResponse } from "next/server";
3+
4+
import { isLocal } from "@/server/isLocal";
5+
import { getDocsDomainEdge } from "@/server/xfernhost/edge";
6+
7+
export async function GET(req: NextRequest) {
8+
if (!isLocal()) {
9+
return NextResponse.json(
10+
{
11+
error:
12+
"local revalidation is not accessible outside of local development mode",
13+
},
14+
{ status: 400 }
15+
);
16+
}
17+
18+
try {
19+
// Revalidate the provided tag
20+
const domain = getDocsDomainEdge(req);
21+
revalidateTag(domain);
22+
23+
return NextResponse.json({
24+
revalidated: true,
25+
domain,
26+
now: Date.now(),
27+
});
28+
} catch (error) {
29+
return NextResponse.json(
30+
{
31+
error: "[revalidate-local] failed to revalidate",
32+
message: error instanceof Error ? error.message : "unknown error",
33+
},
34+
{ status: 500 }
35+
);
36+
}
37+
}

packages/fern-docs/bundle/src/app/layout.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { experimental_taintUniqueValue } from "react";
44
import { ConsoleMessage } from "@/components/console-message";
55
import { FERN_DOCS_ID } from "@/components/constants";
66
import { ScrollToTop } from "@/components/layouts/ScrollToTop";
7+
import { WebSocketRefresh } from "@/components/websocket-refresh";
8+
import { isLocal } from "@/server/isLocal";
79

810
import "./globals.css";
911
import { Providers } from "./providers";
@@ -55,6 +57,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
5557
<body className="antialiased" id={FERN_DOCS_ID}>
5658
<ConsoleMessage />
5759
<ScrollToTop />
60+
{isLocal() && <WebSocketRefresh />}
5861
<Providers>{children}</Providers>
5962
</body>
6063
</html>

packages/fern-docs/bundle/src/app/sitemap.ts

+5
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ import { getCanonicalUrl } from "@fern-docs/edge-config";
88
import { conformTrailingSlash } from "@fern-docs/utils";
99

1010
import { createCachedDocsLoader } from "@/server/docs-loader";
11+
import { isLocal } from "@/server/isLocal";
1112
import { getDocsDomainApp, getDocsHostApp } from "@/server/xfernhost/app";
1213

1314
import { getFernToken } from "./fern-token";
1415

1516
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
17+
if (isLocal()) {
18+
return [];
19+
}
20+
1621
const host = await getDocsHostApp();
1722
const domain = await getDocsDomainApp();
1823
const canonicalUrl = await getCanonicalUrl(domain);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
5+
import { isLocal } from "@/server/isLocal";
6+
7+
export function WebSocketRefresh() {
8+
useEffect(() => {
9+
if (!isLocal()) {
10+
console.log("Not in local mode, skipping WebSocket connection");
11+
return;
12+
}
13+
14+
// Ensure we're in a browser environment
15+
if (typeof window === "undefined") {
16+
console.log("Not in browser environment, skipping WebSocket connection");
17+
return;
18+
}
19+
20+
// Ensure WebSocket is available
21+
if (typeof WebSocket === "undefined") {
22+
console.error("WebSocket is not available in this environment");
23+
return;
24+
}
25+
26+
// Get the current port from the window location
27+
const port = process.env.NEXT_PUBLIC_FDR_ORIGIN || 3001;
28+
const wsUrl = `ws://localhost:${port}`;
29+
30+
console.log(`Attempting to connect to WebSocket server at ${wsUrl}...`);
31+
32+
let ws: WebSocket | null = null;
33+
let timeout: NodeJS.Timeout | null = null;
34+
35+
try {
36+
ws = new WebSocket(wsUrl);
37+
38+
ws.onopen = () => {
39+
console.log("Client: Successfully connected to WebSocket server");
40+
};
41+
42+
ws.onmessage = async (event) => {
43+
console.log("Client: Received WebSocket message:", event.data);
44+
45+
try {
46+
const message = JSON.parse(event.data);
47+
console.log("Client: Parsed message:", message);
48+
49+
if (message.type === "finishReload") {
50+
console.log(
51+
"Client: Received finishReload message, calling revalidate-local endpoint"
52+
);
53+
try {
54+
const response = await fetch("/api/fern-docs/revalidate-local");
55+
if (!response.ok) {
56+
throw new Error(`HTTP error! status: ${response.status}`);
57+
}
58+
console.log("Client: Successfully revalidated");
59+
// Reload the page after revalidation
60+
window.location.reload();
61+
} catch (error) {
62+
console.error("Client: Failed to revalidate:", error);
63+
}
64+
}
65+
} catch (error) {
66+
console.error("Client: Failed to parse WebSocket message:", error);
67+
}
68+
};
69+
70+
ws.onerror = (error) => {
71+
console.error("Client: WebSocket error:", error);
72+
};
73+
74+
ws.onclose = (event) => {
75+
console.log(
76+
`Client: WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}`
77+
);
78+
};
79+
80+
// Add connection timeout check
81+
timeout = setTimeout(() => {
82+
if (ws?.readyState !== WebSocket.OPEN) {
83+
console.error(
84+
"Client: WebSocket connection failed to establish within 5 seconds"
85+
);
86+
}
87+
}, 5000);
88+
} catch (error) {
89+
console.error("Client: Failed to create WebSocket connection:", error);
90+
}
91+
92+
return () => {
93+
console.log("Client: Cleaning up WebSocket connection");
94+
if (timeout) {
95+
clearTimeout(timeout);
96+
}
97+
if (ws) {
98+
ws.close();
99+
}
100+
};
101+
}, []);
102+
103+
return null;
104+
}

packages/fern-docs/bundle/src/middleware.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,14 @@ export const middleware: NextMiddleware = async (request) => {
193193
if (isLocal()) {
194194
// serve local files directly
195195
if (pathname.startsWith("/_local/")) {
196-
return NextResponse.next({
197-
request: { headers },
198-
});
196+
const origin = process.env.NEXT_PUBLIC_FDR_ORIGIN;
197+
if (!origin) {
198+
throw new Error(
199+
"NEXT_PUBLIC_FDR_ORIGIN is required for local file handling"
200+
);
201+
}
202+
const absoluteUrl = new URL(pathname, origin);
203+
return NextResponse.redirect(absoluteUrl);
199204
}
200205

201206
const getResponse = () => {

packages/fern-docs/bundle/src/server/loadWithUrl.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { unstable_cache } from "next/cache";
22
import { notFound } from "next/navigation";
33
import { cache } from "react";
44

5-
import { Url } from "@fern-api/fdr-sdk/api-definition";
65
import { APIResponse, FdrAPI } from "@fern-api/fdr-sdk/client/types";
76
import { isPreviewDomain, withoutStaging } from "@fern-docs/utils";
87

@@ -32,7 +31,7 @@ export const loadWithUrl = cache(
3231
if (isLocal()) {
3332
const response =
3433
await provideRegistryService().docs.v2.read.getDocsForUrl({
35-
url: Url(""),
34+
url: FdrAPI.Url("/"),
3635
});
3736
if (response.ok) {
3837
return response.body;

0 commit comments

Comments
 (0)