diff --git a/test-apps/remix-vite/app/routes/_layout.added.tsx b/test-apps/react-router-vite/app/routes/_layout.added.tsx
similarity index 100%
rename from test-apps/remix-vite/app/routes/_layout.added.tsx
rename to test-apps/react-router-vite/app/routes/_layout.added.tsx
diff --git a/test-apps/remix-vite/app/routes/_layout.final_test.tsx b/test-apps/react-router-vite/app/routes/_layout.final_test.tsx
similarity index 62%
rename from test-apps/remix-vite/app/routes/_layout.final_test.tsx
rename to test-apps/react-router-vite/app/routes/_layout.final_test.tsx
index c1f6998..dcac1a8 100644
--- a/test-apps/remix-vite/app/routes/_layout.final_test.tsx
+++ b/test-apps/react-router-vite/app/routes/_layout.final_test.tsx
@@ -1,7 +1,6 @@
-import type { V2_MetaFunction } from "@remix-run/react/dist/routeModules";
-import type { HeadersFunction, LinksFunction, LoaderArgs, ActionArgs } from "@remix-run/node";
-import { useLoaderData, isRouteErrorResponse, useRouteError } from "@remix-run/react";
-import type { ShouldRevalidateFunction } from "@remix-run/react";
+
+import { useLoaderData, isRouteErrorResponse, useRouteError } from "react-router";
+import type { ActionFunctionArgs, HeadersFunction, LinksFunction, LoaderFunctionArgs, MetaFunction, ShouldRevalidateFunction } from "react-router";
export const links: LinksFunction = () => (
[
@@ -9,7 +8,7 @@ export const links: LinksFunction = () => (
]
);
-export const meta: V2_MetaFunction = () => [
+export const meta: MetaFunction = () => [
// your meta here
];
@@ -23,11 +22,11 @@ export const headers: HeadersFunction = () => (
}
);
-export const loader = async ({ request }: LoaderArgs) => {
+export const loader = async ({ request }: LoaderFunctionArgs) => {
return null;
};
-export const action = async ({ request }: ActionArgs) => {
+export const action = async ({ request }: ActionFunctionArgs) => {
return null;
};
diff --git a/test-apps/remix-vite/app/routes/_layout.new_route.tsx b/test-apps/react-router-vite/app/routes/_layout.new_route.tsx
similarity index 100%
rename from test-apps/remix-vite/app/routes/_layout.new_route.tsx
rename to test-apps/react-router-vite/app/routes/_layout.new_route.tsx
diff --git a/test-apps/remix-vite/app/routes/_layout.one_more_time.tsx b/test-apps/react-router-vite/app/routes/_layout.one_more_time.tsx
similarity index 62%
rename from test-apps/remix-vite/app/routes/_layout.one_more_time.tsx
rename to test-apps/react-router-vite/app/routes/_layout.one_more_time.tsx
index c1f6998..dcac1a8 100644
--- a/test-apps/remix-vite/app/routes/_layout.one_more_time.tsx
+++ b/test-apps/react-router-vite/app/routes/_layout.one_more_time.tsx
@@ -1,7 +1,6 @@
-import type { V2_MetaFunction } from "@remix-run/react/dist/routeModules";
-import type { HeadersFunction, LinksFunction, LoaderArgs, ActionArgs } from "@remix-run/node";
-import { useLoaderData, isRouteErrorResponse, useRouteError } from "@remix-run/react";
-import type { ShouldRevalidateFunction } from "@remix-run/react";
+
+import { useLoaderData, isRouteErrorResponse, useRouteError } from "react-router";
+import type { ActionFunctionArgs, HeadersFunction, LinksFunction, LoaderFunctionArgs, MetaFunction, ShouldRevalidateFunction } from "react-router";
export const links: LinksFunction = () => (
[
@@ -9,7 +8,7 @@ export const links: LinksFunction = () => (
]
);
-export const meta: V2_MetaFunction = () => [
+export const meta: MetaFunction = () => [
// your meta here
];
@@ -23,11 +22,11 @@ export const headers: HeadersFunction = () => (
}
);
-export const loader = async ({ request }: LoaderArgs) => {
+export const loader = async ({ request }: LoaderFunctionArgs) => {
return null;
};
-export const action = async ({ request }: ActionArgs) => {
+export const action = async ({ request }: ActionFunctionArgs) => {
return null;
};
diff --git a/test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.test/route.tsx b/test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.test/route.tsx
similarity index 92%
rename from test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.test/route.tsx
rename to test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.test/route.tsx
index 7b5b9c8..428227d 100644
--- a/test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.test/route.tsx
+++ b/test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.test/route.tsx
@@ -1,7 +1,6 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { json, type LoaderFunctionArgs } from "@remix-run/node";
-import type { MetaFunction } from "@remix-run/node";
-import { Link, useFetcher, useLoaderData, useSubmit } from "@remix-run/react";
+import { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs, useLoaderData, useFetcher, useSubmit, Link, data } from "react-router";
+
+
export const meta: MetaFunction = () => {
return [
@@ -12,7 +11,7 @@ export const meta: MetaFunction = () => {
export const loader = async ({ request }: LoaderFunctionArgs) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
- return json({
+ return data({
should: "work",
with: {
nested: {
diff --git a/test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.tsx b/test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.tsx
similarity index 92%
rename from test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.tsx
rename to test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.tsx
index 7b5b9c8..75a4278 100644
--- a/test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.tsx
+++ b/test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.$test.$wildcard.tsx
@@ -1,7 +1,5 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { json, type LoaderFunctionArgs } from "@remix-run/node";
-import type { MetaFunction } from "@remix-run/node";
-import { Link, useFetcher, useLoaderData, useSubmit } from "@remix-run/react";
+
+import { ActionFunctionArgs, data, Link, LoaderFunctionArgs, MetaFunction, useFetcher, useLoaderData, useSubmit } from "react-router";
export const meta: MetaFunction = () => {
return [
@@ -12,7 +10,7 @@ export const meta: MetaFunction = () => {
export const loader = async ({ request }: LoaderFunctionArgs) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
- return json({
+ return data({
should: "work",
with: {
nested: {
diff --git a/test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.$test.tsx b/test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.$test.tsx
similarity index 73%
rename from test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.$test.tsx
rename to test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.$test.tsx
index e75cdb2..be2b479 100644
--- a/test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.$test.tsx
+++ b/test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.$test.tsx
@@ -1,7 +1,5 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { json, type LoaderFunctionArgs } from "@remix-run/node";
-import type { MetaFunction } from "@remix-run/node";
-import { Link, Outlet, useFetcher, useLoaderData, useSubmit } from "@remix-run/react";
+
+import { ActionFunctionArgs, data, Link, LoaderFunctionArgs, MetaFunction, Outlet, useFetcher, useLoaderData, useSubmit } from "react-router";
export const meta: MetaFunction = () => {
return [
@@ -11,14 +9,14 @@ export const meta: MetaFunction = () => {
};
export const loader = async ({ request }: LoaderFunctionArgs) => {
- return json({
+ return data({
should: "work",
with: {
nested: {
objects: {
-
+
},
- },
+ },
},
}, { headers: { "Cache-Control": "max-age=3600, private" } });
};
@@ -34,7 +32,7 @@ export default function IndexRoute() {
const pFetcher = useFetcher();
const submit = useSubmit();
const data = new FormData();
- data.append("test", "test");
+ data.append("test", "test");
return (
Welcome to Remix 4
diff --git a/test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.tsx b/test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.tsx
similarity index 87%
rename from test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.tsx
rename to test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.tsx
index 2a5c91c..d74bdff 100644
--- a/test-apps/remix-vite/app/routes/_layout.tests.$id.edit.new.tsx
+++ b/test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.tsx
@@ -1,7 +1,5 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { json, type LoaderFunctionArgs } from "@remix-run/node";
-import type { MetaFunction } from "@remix-run/node";
-import { Link, Outlet, useFetcher, useLoaderData, useSubmit } from "@remix-run/react";
+
+import { ActionFunctionArgs, data, Link, LoaderFunctionArgs, MetaFunction, Outlet, useFetcher, useLoaderData, useSubmit } from "react-router";
export const meta: MetaFunction = () => {
return [
@@ -11,7 +9,7 @@ export const meta: MetaFunction = () => {
};
export const loader = async ({ request }: LoaderFunctionArgs) => {
- return json({
+ return data({
should: "work",
with: {
nested: {
@@ -79,7 +77,7 @@ export default function IndexRoute() {
-
+
Remix Docs
diff --git a/test-apps/remix-vite/app/routes/_layout.tests.$id.edit.tsx b/test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.tsx
similarity index 70%
rename from test-apps/remix-vite/app/routes/_layout.tests.$id.edit.tsx
rename to test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.tsx
index bb67ab9..0077905 100644
--- a/test-apps/remix-vite/app/routes/_layout.tests.$id.edit.tsx
+++ b/test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.tsx
@@ -1,13 +1,14 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { json, redirect, type LoaderFunctionArgs } from "@remix-run/node";
-import type { MetaFunction } from "@remix-run/node";
+
import {
+ ActionFunctionArgs,
Link,
+ LoaderFunctionArgs,
+ MetaFunction,
Outlet,
useFetcher,
useLoaderData,
useSubmit,
-} from "@remix-run/react";
+} from "react-router";
export const meta: MetaFunction = () => {
return [
@@ -17,11 +18,11 @@ export const meta: MetaFunction = () => {
};
export const loader = ({ request }: LoaderFunctionArgs) => {
- return json({ test: "died" }, )
+ return { test: "died" }
};
export const action = async ({ request }: ActionFunctionArgs) => {
- return json({
+ return ({
test: "died",
});
};
@@ -33,7 +34,7 @@ export default function Index() {
-
+
diff --git a/test-apps/remix-vite/app/routes/_layout.tests.$id.tsx b/test-apps/react-router-vite/app/routes/_layout.tests.$id.tsx
similarity index 80%
rename from test-apps/remix-vite/app/routes/_layout.tests.$id.tsx
rename to test-apps/react-router-vite/app/routes/_layout.tests.$id.tsx
index d805036..9063ecd 100644
--- a/test-apps/remix-vite/app/routes/_layout.tests.$id.tsx
+++ b/test-apps/react-router-vite/app/routes/_layout.tests.$id.tsx
@@ -1,13 +1,15 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { json, redirect, type LoaderFunctionArgs } from "@remix-run/node";
-import type { MetaFunction } from "@remix-run/node";
+
import {
+ ActionFunctionArgs,
+ json,
Link,
+ LoaderFunctionArgs,
+ MetaFunction,
Outlet,
useFetcher,
useLoaderData,
useSubmit,
-} from "@remix-run/react";
+} from "react-router";
export const meta: MetaFunction = () => {
return [
@@ -25,7 +27,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
};
export const action = async ({ request }: ActionFunctionArgs) => {
- return json({
+ return ({
test: "died",
});
};
diff --git a/test-apps/remix-vite/app/routes/_layout.tests.tsx b/test-apps/react-router-vite/app/routes/_layout.tests.tsx
similarity index 78%
rename from test-apps/remix-vite/app/routes/_layout.tests.tsx
rename to test-apps/react-router-vite/app/routes/_layout.tests.tsx
index 7438d2a..0e401a1 100644
--- a/test-apps/remix-vite/app/routes/_layout.tests.tsx
+++ b/test-apps/react-router-vite/app/routes/_layout.tests.tsx
@@ -1,13 +1,14 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { json, redirect, type LoaderFunctionArgs } from "@remix-run/node";
-import type { MetaFunction } from "@remix-run/node";
+
import {
+ ActionFunctionArgs,
Link,
+ LoaderFunctionArgs,
+ MetaFunction,
Outlet,
useFetcher,
useLoaderData,
useSubmit,
-} from "@remix-run/react";
+} from "react-router";
export const meta: MetaFunction = () => {
return [
@@ -21,7 +22,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
};
export const action = async ({ request }: ActionFunctionArgs) => {
- return json({
+ return ({
test: "died",
});
};
diff --git a/test-apps/remix-vite/app/routes/_layout.tsx b/test-apps/react-router-vite/app/routes/_layout.tsx
similarity index 66%
rename from test-apps/remix-vite/app/routes/_layout.tsx
rename to test-apps/react-router-vite/app/routes/_layout.tsx
index 8482389..aa7975e 100644
--- a/test-apps/remix-vite/app/routes/_layout.tsx
+++ b/test-apps/react-router-vite/app/routes/_layout.tsx
@@ -1,5 +1,5 @@
-import type { LoaderFunctionArgs } from "@remix-run/node";
-import { Outlet } from "@remix-run/react";
+
+import { LoaderFunctionArgs, Outlet } from "react-router";
export const loader = async ({ request }: LoaderFunctionArgs) => {
return { test: "returning raw object" };
diff --git a/test-apps/react-router-vite/app/routes/_layout/test.tsx b/test-apps/react-router-vite/app/routes/_layout/test.tsx
new file mode 100644
index 0000000..9e4f3c9
--- /dev/null
+++ b/test-apps/react-router-vite/app/routes/_layout/test.tsx
@@ -0,0 +1,13 @@
+
+import { LoaderFunctionArgs, useLoaderData } from "react-router";
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ return null;
+};
+
+export default function RouteComponent(){
+ const data = useLoaderData
()
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/test-apps/remix-vite/app/routes/correct.tsx b/test-apps/react-router-vite/app/routes/correct.tsx
similarity index 62%
rename from test-apps/remix-vite/app/routes/correct.tsx
rename to test-apps/react-router-vite/app/routes/correct.tsx
index c1f6998..dcac1a8 100644
--- a/test-apps/remix-vite/app/routes/correct.tsx
+++ b/test-apps/react-router-vite/app/routes/correct.tsx
@@ -1,7 +1,6 @@
-import type { V2_MetaFunction } from "@remix-run/react/dist/routeModules";
-import type { HeadersFunction, LinksFunction, LoaderArgs, ActionArgs } from "@remix-run/node";
-import { useLoaderData, isRouteErrorResponse, useRouteError } from "@remix-run/react";
-import type { ShouldRevalidateFunction } from "@remix-run/react";
+
+import { useLoaderData, isRouteErrorResponse, useRouteError } from "react-router";
+import type { ActionFunctionArgs, HeadersFunction, LinksFunction, LoaderFunctionArgs, MetaFunction, ShouldRevalidateFunction } from "react-router";
export const links: LinksFunction = () => (
[
@@ -9,7 +8,7 @@ export const links: LinksFunction = () => (
]
);
-export const meta: V2_MetaFunction = () => [
+export const meta: MetaFunction = () => [
// your meta here
];
@@ -23,11 +22,11 @@ export const headers: HeadersFunction = () => (
}
);
-export const loader = async ({ request }: LoaderArgs) => {
+export const loader = async ({ request }: LoaderFunctionArgs) => {
return null;
};
-export const action = async ({ request }: ActionArgs) => {
+export const action = async ({ request }: ActionFunctionArgs) => {
return null;
};
diff --git a/test-apps/remix-vite/app/routes/dashboard.tsx b/test-apps/react-router-vite/app/routes/dashboard.tsx
similarity index 75%
rename from test-apps/remix-vite/app/routes/dashboard.tsx
rename to test-apps/react-router-vite/app/routes/dashboard.tsx
index 64cde7a..f378d1c 100644
--- a/test-apps/remix-vite/app/routes/dashboard.tsx
+++ b/test-apps/react-router-vite/app/routes/dashboard.tsx
@@ -1,8 +1,8 @@
-import type { LoaderFunctionArgs } from '@remix-run/node';
-import { useLoaderData, Form } from '@remix-run/react';
+
+import { useLoaderData, Form, LoaderFunctionArgs } from 'react-router';
export const loader = async ({ request }: LoaderFunctionArgs) => {
-
+
return { message: 'You are logged in!' };
};
diff --git a/test-apps/remix-vite/app/routes/embedded.tsx b/test-apps/react-router-vite/app/routes/embedded.tsx
similarity index 81%
rename from test-apps/remix-vite/app/routes/embedded.tsx
rename to test-apps/react-router-vite/app/routes/embedded.tsx
index f67d5f4..5e0bede 100644
--- a/test-apps/remix-vite/app/routes/embedded.tsx
+++ b/test-apps/react-router-vite/app/routes/embedded.tsx
@@ -1,8 +1,6 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { redirect, type LoaderFunctionArgs, defer } from "@remix-run/node";
-import type { MetaFunction } from "@remix-run/node";
-import { Link, useFetcher, useSubmit } from "@remix-run/react";
-import { EmbeddedDevTools } from "remix-development-tools/client";
+
+import { Link, LoaderFunctionArgs, MetaFunction, useFetcher, useSubmit } from "react-router";
+import { EmbeddedDevTools } from "react-router-devtools/client";
export const meta: MetaFunction = () => {
return [
@@ -17,14 +15,12 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
resolve("test");
}, 1000);
})
- return defer({ message: "Hello World!", test });
+ return ({ message: "Hello World!", test });
};
-export const action = async ({ request }: ActionFunctionArgs) => {
- return redirect("/login");
-};
-export default function Index() {
+
+export default function Index() {
const lFetcher = useFetcher();
const pFetcher = useFetcher();
const submit = useSubmit();
diff --git a/test-apps/remix-vite/app/routes/file.tsx b/test-apps/react-router-vite/app/routes/file.tsx
similarity index 72%
rename from test-apps/remix-vite/app/routes/file.tsx
rename to test-apps/react-router-vite/app/routes/file.tsx
index cd60bc6..8467945 100644
--- a/test-apps/remix-vite/app/routes/file.tsx
+++ b/test-apps/react-router-vite/app/routes/file.tsx
@@ -1,4 +1,4 @@
-import { Outlet } from "@remix-run/react";
+import { Outlet } from "react-router";
export default function File() {
return (
diff --git a/test-apps/remix-vite/app/routes/folder/route.tsx b/test-apps/react-router-vite/app/routes/folder/route.tsx
similarity index 72%
rename from test-apps/remix-vite/app/routes/folder/route.tsx
rename to test-apps/react-router-vite/app/routes/folder/route.tsx
index b66d2c0..447e7ab 100644
--- a/test-apps/remix-vite/app/routes/folder/route.tsx
+++ b/test-apps/react-router-vite/app/routes/folder/route.tsx
@@ -1,4 +1,4 @@
-import { Outlet } from "@remix-run/react";
+import { Outlet } from "react-router";
export default function Folder() {
return
diff --git a/test-apps/remix-vite/app/routes/login.tsx b/test-apps/react-router-vite/app/routes/login.tsx
similarity index 58%
rename from test-apps/remix-vite/app/routes/login.tsx
rename to test-apps/react-router-vite/app/routes/login.tsx
index 7a946f6..a074f3c 100644
--- a/test-apps/remix-vite/app/routes/login.tsx
+++ b/test-apps/react-router-vite/app/routes/login.tsx
@@ -1,15 +1,15 @@
-import { Form, Link } from "@remix-run/react";
+import { Form, Link } from "react-router";
-interface SocialButtonProps {
+interface SocialButtonProps {
label: string;
}
-
+
export default function LoginRoute() {
return (
<>
Login
-
+
>
);
}
diff --git a/test-apps/remix-vite/app/routes/logout.tsx b/test-apps/react-router-vite/app/routes/logout.tsx
similarity index 59%
rename from test-apps/remix-vite/app/routes/logout.tsx
rename to test-apps/react-router-vite/app/routes/logout.tsx
index 8ac305c..dba357d 100644
--- a/test-apps/remix-vite/app/routes/logout.tsx
+++ b/test-apps/react-router-vite/app/routes/logout.tsx
@@ -1,4 +1,6 @@
-import type { LoaderFunctionArgs } from "@remix-run/node"
+import { LoaderFunctionArgs } from "react-router";
+
+
export const action = async ({ request }: LoaderFunctionArgs) => {
return null;
diff --git a/test-apps/remix-vite/app/routes/other._index.tsx b/test-apps/react-router-vite/app/routes/other._index.tsx
similarity index 100%
rename from test-apps/remix-vite/app/routes/other._index.tsx
rename to test-apps/react-router-vite/app/routes/other._index.tsx
diff --git a/test-apps/remix-vite/app/routes/other.page.tsx b/test-apps/react-router-vite/app/routes/other.page.tsx
similarity index 100%
rename from test-apps/remix-vite/app/routes/other.page.tsx
rename to test-apps/react-router-vite/app/routes/other.page.tsx
diff --git a/test-apps/remix-vite/app/routes/other.tsx b/test-apps/react-router-vite/app/routes/other.tsx
similarity index 73%
rename from test-apps/remix-vite/app/routes/other.tsx
rename to test-apps/react-router-vite/app/routes/other.tsx
index 1b1a7ea..854f8d9 100644
--- a/test-apps/remix-vite/app/routes/other.tsx
+++ b/test-apps/react-router-vite/app/routes/other.tsx
@@ -1,4 +1,4 @@
-import { Outlet } from "@remix-run/react";
+import { Outlet } from "react-router";
export default function OtherLayout() {
return
diff --git a/test-apps/remix-vite/app/routes/server-timings.tsx b/test-apps/react-router-vite/app/routes/server-timings.tsx
similarity index 70%
rename from test-apps/remix-vite/app/routes/server-timings.tsx
rename to test-apps/react-router-vite/app/routes/server-timings.tsx
index 6e02b13..c2535f0 100644
--- a/test-apps/remix-vite/app/routes/server-timings.tsx
+++ b/test-apps/react-router-vite/app/routes/server-timings.tsx
@@ -1,7 +1,6 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { json, redirect, type LoaderFunctionArgs, defer } from "@remix-run/node";
-import type { MetaFunction } from "@remix-run/node";
+
+import { ActionFunctionArgs, data, json, LoaderFunctionArgs, MetaFunction, redirect } from "react-router";
import { getServerTiming } from "~/timing.server";
@@ -13,7 +12,7 @@ export const meta: MetaFunction = () => {
};
-export const loader = async ({ request, response }: LoaderFunctionArgs) => {
+export const loader = async ({ request, }: LoaderFunctionArgs) => {
const { time, getServerTimingHeader } = getServerTiming();
await time("test", () => {
return new Promise((resolve, reject) => {
@@ -27,7 +26,7 @@ export const loader = async ({ request, response }: LoaderFunctionArgs) => {
resolve("test");
}, 200);
}))
- return json({ message: "Hello World!", }, {
+ return data({ message: "Hello World!", }, {
headers: getServerTimingHeader(),
});
};
diff --git a/test-apps/react-router-vite/app/routes/tester.tsx b/test-apps/react-router-vite/app/routes/tester.tsx
new file mode 100644
index 0000000..3e1133a
--- /dev/null
+++ b/test-apps/react-router-vite/app/routes/tester.tsx
@@ -0,0 +1,33 @@
+import { LoaderFunctionArgs, ClientLoaderFunctionArgs, ClientActionFunctionArgs, ActionFunctionArgs } from "react-router";
+import { useLoaderData, isRouteErrorResponse, useRouteError } from "react-router";
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ return null;
+};
+
+export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
+ return null;
+};
+
+export const action = async ({ request }: ActionFunctionArgs) => {
+ return null;
+};
+
+export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
+ return null;
+};
+
+export default function RouteComponent(){
+ const data = useLoaderData
()
+ return (
+
+ );
+}
+
+export function ErrorBoundary(){
+ const error = useRouteError();
+ if (isRouteErrorResponse(error)) {
+ return
+ }
+ return
+}
\ No newline at end of file
diff --git a/test-apps/remix-vite/app/timing.server.ts b/test-apps/react-router-vite/app/timing.server.ts
similarity index 100%
rename from test-apps/remix-vite/app/timing.server.ts
rename to test-apps/react-router-vite/app/timing.server.ts
diff --git a/test-apps/react-router-vite/env.d.ts b/test-apps/react-router-vite/env.d.ts
new file mode 100644
index 0000000..5e7dfe5
--- /dev/null
+++ b/test-apps/react-router-vite/env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/test-apps/react-router-vite/package.json b/test-apps/react-router-vite/package.json
new file mode 100644
index 0000000..cdb60ce
--- /dev/null
+++ b/test-apps/react-router-vite/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "remix-vite",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "build": "react-router build",
+ "dev": "react-router dev",
+ "start": "react-router-serve ./build/server/index.js",
+ "typecheck": "tsc"
+ },
+ "dependencies": {
+ "react-router": "7.0.0",
+ "isbot": "^5.1.17",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "@react-router/dev": "7.0.0",
+ "@react-router/node": "7.0.0",
+ "@react-router/serve": "7.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.11",
+ "@types/react-dom": "^18.3.1",
+ "@react-router/fs-routes": "7.0.0",
+ "eslint": "^9.13.0",
+ "typescript": "^5.6.3",
+ "vite": "^5.4.9",
+ "vite-tsconfig-paths": "^5.0.1",
+ "react-router-devtools": "*",
+ "vite-plugin-inspect": "^0.8.7"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+}
\ No newline at end of file
diff --git a/test-apps/remix-vite/plugins/tailwind-palette.tsx b/test-apps/react-router-vite/plugins/tailwind-palette.tsx
similarity index 100%
rename from test-apps/remix-vite/plugins/tailwind-palette.tsx
rename to test-apps/react-router-vite/plugins/tailwind-palette.tsx
diff --git a/test-apps/remix-vite/public/favicon.ico b/test-apps/react-router-vite/public/favicon.ico
similarity index 100%
rename from test-apps/remix-vite/public/favicon.ico
rename to test-apps/react-router-vite/public/favicon.ico
diff --git a/test-apps/remix-vite/tailwind.config.js b/test-apps/react-router-vite/tailwind.config.js
similarity index 100%
rename from test-apps/remix-vite/tailwind.config.js
rename to test-apps/react-router-vite/tailwind.config.js
diff --git a/test-apps/remix-vite/tsconfig.json b/test-apps/react-router-vite/tsconfig.json
similarity index 100%
rename from test-apps/remix-vite/tsconfig.json
rename to test-apps/react-router-vite/tsconfig.json
diff --git a/test-apps/remix-vite/vite.config.ts b/test-apps/react-router-vite/vite.config.ts
similarity index 65%
rename from test-apps/remix-vite/vite.config.ts
rename to test-apps/react-router-vite/vite.config.ts
index 8bf23bd..08b4dde 100644
--- a/test-apps/remix-vite/vite.config.ts
+++ b/test-apps/react-router-vite/vite.config.ts
@@ -1,12 +1,12 @@
-import { vitePlugin as remix } from "@remix-run/dev";
+import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
-import { remixDevTools, defineRdtConfig } from "remix-development-tools"
+import { reactRouterDevTools, defineRdtConfig } from "react-router-devtools"
+import inspect from "vite-plugin-inspect"
const config = defineRdtConfig({
client: {
defaultOpen: false,
- panelLocation: "top",
position: "top-right",
requireUrlFlag: false,
liveUrls: [
@@ -17,7 +17,10 @@ const config = defineRdtConfig({
}],
},
pluginDir: "./plugins",
- includeInProd: true,
+ includeInProd: {
+ client: true,
+ server: true
+ },
// Set this option to true to suppress deprecation warnings
// suppressDeprecationWarning: true,
server: {
@@ -27,14 +30,14 @@ const config = defineRdtConfig({
export default defineConfig({
plugins: [
- remixDevTools( config),
- remix({
- future: {
- unstable_singleFetch: true
- }
- }),
+ inspect(),
+ reactRouterDevTools( config),
+ reactRouter(),
tsconfigPaths()
],
+ optimizeDeps: {
+ exclude: ["react-router-devtools"]
+ },
server: {
open: true,
port: 3005,
diff --git a/test-apps/remix-vite/app/root.tsx b/test-apps/remix-vite/app/root.tsx
deleted file mode 100644
index 727c9c1..0000000
--- a/test-apps/remix-vite/app/root.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import {
- Form,
- Links,
- LiveReload,
- Meta,
- Outlet,
- Scripts,
- ScrollRestoration,
-} from "@remix-run/react";
-import { LinksFunction, json } from "@remix-run/server-runtime";
-import { userSomething } from "./modules/user.server";
-
-
-export function links() {
- return []
-}
-
-export const loader = () => {
- console.log("loader?")
- userSomething();
- return ({ message: "Hello World" });
-}
-
-export const action = () => {
- return json({ message: "Hello World" });
-}
-
-export default function App() {
- console.log("App?")
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/test-apps/remix-vite/app/routes/_layout/test.tsx b/test-apps/remix-vite/app/routes/_layout/test.tsx
deleted file mode 100644
index 054f645..0000000
--- a/test-apps/remix-vite/app/routes/_layout/test.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { LoaderArgs } from "@remix-run/node";
-import { useLoaderData } from "@remix-run/react";
-
-export const loader = async ({ request }: LoaderArgs) => {
- return null;
-};
-
-export default function RouteComponent(){
- const data = useLoaderData()
- return (
-
- );
-}
\ No newline at end of file
diff --git a/test-apps/remix-vite/env.d.ts b/test-apps/remix-vite/env.d.ts
deleted file mode 100644
index 78ed234..0000000
--- a/test-apps/remix-vite/env.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-///
-///
diff --git a/test-apps/remix-vite/package.json b/test-apps/remix-vite/package.json
deleted file mode 100644
index 97d8bcf..0000000
--- a/test-apps/remix-vite/package.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "name": "remix-vite",
- "private": true,
- "sideEffects": false,
- "type": "module",
- "scripts": {
- "build": "remix vite:build",
- "dev": "remix vite:dev",
- "start": "remix-serve ./build/server/index.js",
- "typecheck": "tsc"
- },
- "dependencies": {
- "@remix-run/node": "^2.8.0",
- "@remix-run/react": "^2.8.0",
- "@remix-run/serve": "^2.8.0",
- "isbot": "^3.6.8",
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
- },
- "devDependencies": {
- "@remix-run/dev": "^2.8.0",
- "@remix-run/eslint-config": "^2.8.0",
- "@types/react": "^18.2.20",
- "@types/react-dom": "^18.2.7",
- "eslint": "^8.38.0",
- "typescript": "^5.1.6",
- "vite": "^5.1.0",
- "vite-tsconfig-paths": "^4.2.1",
- "remix-development-tools": "*"
- },
- "engines": {
- "node": ">=18.0.0"
- }
-}
\ No newline at end of file
diff --git a/test-apps/remix-website/.dockerignore b/test-apps/remix-website/.dockerignore
deleted file mode 100644
index 91077d0..0000000
--- a/test-apps/remix-website/.dockerignore
+++ /dev/null
@@ -1,7 +0,0 @@
-/node_modules
-*.log
-.DS_Store
-.env
-/.cache
-/public/build
-/build
diff --git a/test-apps/remix-website/.env.example b/test-apps/remix-website/.env.example
deleted file mode 100644
index 3bda642..0000000
--- a/test-apps/remix-website/.env.example
+++ /dev/null
@@ -1,18 +0,0 @@
-# A token to increase the rate limiting from 60/hr to 1000/hr
-GITHUB_TOKEN=""
-
-# GitHub repo to pull docs from
-SOURCE_REPO="remix-run/remix"
-
-# Package from which to base docs version
-RELEASE_PACKAGE="remix"
-
-# For development, reading the docs from a local repo
-LOCAL_REPO_RELATIVE_PATH="../remix"
-
-# Only needed locally if testing newsletter signup -- get from https://app.convertkit.com/account_settings/advanced_settings
-CONVERTKIT_KEY="..."
-
-# Not used locally, but make sure this is set in GitHub for production
-FASTLY_API_TOKEN="..."
-FASTLY_SERVICE_ID="..."
\ No newline at end of file
diff --git a/test-apps/remix-website/.eslintignore b/test-apps/remix-website/.eslintignore
deleted file mode 100644
index d308827..0000000
--- a/test-apps/remix-website/.eslintignore
+++ /dev/null
@@ -1,4 +0,0 @@
-/.cache
-/build
-/public/build
-/.yalc
diff --git a/test-apps/remix-website/.eslintrc b/test-apps/remix-website/.eslintrc
deleted file mode 100644
index 553ee50..0000000
--- a/test-apps/remix-website/.eslintrc
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "extends": [
- "@remix-run/eslint-config",
- "@remix-run/eslint-config/node",
- "@remix-run/eslint-config/jest-testing-library"
- ],
- // we're using vitest which has a very similar API to jest (so the linting
- // plugins work nicely), but it means we have to explicitly set the jest
- // version.
- "settings": {
- "jest": {
- "version": 27
- }
- },
- "rules": {
- "prefer-const": "off"
- }
-}
diff --git a/test-apps/remix-website/.github/workflows/deploy.production.yml b/test-apps/remix-website/.github/workflows/deploy.production.yml
deleted file mode 100644
index 1e42961..0000000
--- a/test-apps/remix-website/.github/workflows/deploy.production.yml
+++ /dev/null
@@ -1,61 +0,0 @@
-name: 🚀 Deploy (production)
-on:
- push:
- branches:
- - main
- paths-ignore:
- - ".vscode/**"
- - "scripts/**"
- - "README.md"
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-env:
- FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
-
-jobs:
- test:
- name: ⚡ Vitest
- runs-on: ubuntu-latest
- steps:
- - name: ⬇️ Checkout repo
- uses: actions/checkout@v4
-
- - name: ⎔ Setup node
- uses: actions/setup-node@v4
- with:
- node-version: 20
-
- - name: 📥 Install deps
- uses: bahmutov/npm-install@v1
-
- - name: ⚡ Run vitest
- run: npm run test -- --coverage
-
- deploy:
- name: 🚀 Deploy
- runs-on: ubuntu-latest
- needs: [test]
- steps:
- - name: ⬇️ Checkout repo
- uses: actions/checkout@v4
-
- - name: 🎈 Setup Fly
- uses: superfly/flyctl-actions/setup-flyctl@1.5
-
- - name: 🚀 Deploy Production
- if: ${{ github.ref == 'refs/heads/main' }}
- run: flyctl deploy --remote-only --config ./fly.production.toml --build-arg SOURCE_REPO="remix-run/remix" --build-arg RELEASE_PACKAGE="remix" --strategy rolling
- env:
- FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
-
- purge:
- name: 🧹 Purge CDN
- runs-on: ubuntu-latest
- needs: [deploy]
- steps:
- - name: 🧹 Purge All
- run: |
- curl -D - -X POST --location "https://api.fastly.com/service/${{ secrets.FASTLY_SERVICE_ID }}/purge_all" -H "Accept: application/json" -H "Fastly-Key: ${{ secrets.FASTLY_API_TOKEN }}" -H "fastly-soft-purge: 1"
diff --git a/test-apps/remix-website/.github/workflows/deploy.staging.yml b/test-apps/remix-website/.github/workflows/deploy.staging.yml
deleted file mode 100644
index 6f5d8d2..0000000
--- a/test-apps/remix-website/.github/workflows/deploy.staging.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-name: 🚀 Deploy (staging)
-on:
- push:
- tags:
- - stage
- branches:
- - dev
- paths-ignore:
- - ".vscode/**"
- - "scripts/**"
- - "README.md"
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-env:
- FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
-
-jobs:
- test:
- name: ⚡ Vitest
- runs-on: ubuntu-latest
- steps:
- - name: ⬇️ Checkout repo
- uses: actions/checkout@v4
-
- - name: ⎔ Setup node
- uses: actions/setup-node@v4
- with:
- node-version: 20
-
- - name: 📥 Install deps
- uses: bahmutov/npm-install@v1
-
- - name: ⚡ Run vitest
- run: npm run test -- --coverage
-
- deploy:
- name: 🚀 Deploy
- runs-on: ubuntu-latest
- needs: [test]
- steps:
- - name: ⬇️ Checkout repo
- uses: actions/checkout@v4
-
- - name: 🎈 Setup Fly
- uses: superfly/flyctl-actions/setup-flyctl@1.5
-
- - name: 🚀 Deploy Staging
- run: flyctl deploy --remote-only --config ./fly.staging.toml --build-arg SOURCE_REPO="remix-run/remix" --build-arg RELEASE_PACKAGE="remix" --strategy bluegreen
- env:
- FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
diff --git a/test-apps/remix-website/.github/workflows/format.yml b/test-apps/remix-website/.github/workflows/format.yml
deleted file mode 100644
index dde789d..0000000
--- a/test-apps/remix-website/.github/workflows/format.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: 💅 Format
-on:
- push:
- branches:
- - main
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- format:
- if: github.repository == 'remix-run/remix-website'
- name: 💅 Format
- runs-on: ubuntu-latest
-
- steps:
- - name: ⬇️ Checkout repo
- uses: actions/checkout@v4
- with:
- token: ${{ secrets.FORMAT_PAT }}
-
- - name: ⎔ Setup node
- uses: actions/setup-node@v4
- with:
- node-version: 20
-
- - name: 📥 Install deps
- uses: bahmutov/npm-install@v1
-
- - name: 💅 Format
- run: npm run format --if-present
-
- - name: 🗝️ Commit
- run: |
- git config --local user.email "hello@remix.run"
- git config --local user.name "Remix Run Bot"
-
- git add .
- if [ -z "$(git status --porcelain)" ]; then
- echo "💿 no formatting changed"
- exit 0
- fi
- git commit -m "chore: format [skip ci]"
- git push
- echo "💿 pushed formatting changes https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)"
diff --git a/test-apps/remix-website/.github/workflows/lint.yml b/test-apps/remix-website/.github/workflows/lint.yml
deleted file mode 100644
index fb0c463..0000000
--- a/test-apps/remix-website/.github/workflows/lint.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: ⬣ Lint
-on:
- push:
- branches:
- - main
- - dev
- - stage
- pull_request:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- lint:
- name: ⬣ Lint
- runs-on: ubuntu-latest
- steps:
- - name: ⬇️ Checkout repo
- uses: actions/checkout@v4
-
- - name: ⎔ Setup node
- uses: actions/setup-node@v4
- with:
- node-version: 20
-
- - name: 📥 Install deps
- uses: bahmutov/npm-install@v1
-
- - name: 🔬 Lint
- run: npm run lint && npm run typecheck
diff --git a/test-apps/remix-website/.github/workflows/test.yml b/test-apps/remix-website/.github/workflows/test.yml
deleted file mode 100644
index df8310f..0000000
--- a/test-apps/remix-website/.github/workflows/test.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: 🧪 Test
-on:
- pull_request:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-env:
- FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
-
-jobs:
- test:
- name: ⚡ Vitest
- runs-on: ubuntu-latest
- steps:
- - name: ⬇️ Checkout repo
- uses: actions/checkout@v4
-
- - name: ⎔ Setup node
- uses: actions/setup-node@v4
- with:
- node-version: 20
-
- - name: 📥 Install deps
- uses: bahmutov/npm-install@v1
-
- - name: ⚡ Run vitest
- run: npm run test -- --coverage
diff --git a/test-apps/remix-website/.gitignore b/test-apps/remix-website/.gitignore
deleted file mode 100644
index 5a079d6..0000000
--- a/test-apps/remix-website/.gitignore
+++ /dev/null
@@ -1,13 +0,0 @@
-.DS_Store
-node_modules
-/.cache
-/build
-/public/build
-/coverage
-/.env
-/.yalc
-yalc.lock
-/.local.tgz
-
-**/gh-docs/__fixture__/tar.tgz
-/NOTES.md
\ No newline at end of file
diff --git a/test-apps/remix-website/.nvmrc b/test-apps/remix-website/.nvmrc
deleted file mode 100644
index 209e3ef..0000000
--- a/test-apps/remix-website/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-20
diff --git a/test-apps/remix-website/.prettierignore b/test-apps/remix-website/.prettierignore
deleted file mode 100644
index d308827..0000000
--- a/test-apps/remix-website/.prettierignore
+++ /dev/null
@@ -1,4 +0,0 @@
-/.cache
-/build
-/public/build
-/.yalc
diff --git a/test-apps/remix-website/.prettierrc b/test-apps/remix-website/.prettierrc
deleted file mode 100644
index b4bfed3..0000000
--- a/test-apps/remix-website/.prettierrc
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "plugins": ["prettier-plugin-tailwindcss"]
-}
diff --git a/test-apps/remix-website/.vscode/extensions.json b/test-apps/remix-website/.vscode/extensions.json
deleted file mode 100644
index 7c53ae8..0000000
--- a/test-apps/remix-website/.vscode/extensions.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "recommendations": ["dbaeumer.vscode-eslint", "bradlc.vscode-tailwindcss"]
-}
diff --git a/test-apps/remix-website/Dockerfile b/test-apps/remix-website/Dockerfile
deleted file mode 100644
index 57929e4..0000000
--- a/test-apps/remix-website/Dockerfile
+++ /dev/null
@@ -1,54 +0,0 @@
-# base node image
-FROM node:20-bullseye-slim as base
-
-# set for base and all layer that inherit from it
-ENV NODE_ENV production
-
-# Consume build arguments for non-secret env vars
-ARG SOURCE_REPO
-ENV SOURCE_REPO=$SOURCE_REPO
-ARG RELEASE_PACKAGE
-ENV RELEASE_PACKAGE=$RELEASE_PACKAGE
-
-# Install all node_modules, including dev
-FROM base as deps
-
-WORKDIR /remixapp
-
-ADD package.json package-lock.json ./
-RUN npm install --include=dev
-
-# Setup production node_modules
-FROM base as production-deps
-
-WORKDIR /remixapp
-
-COPY --from=deps /remixapp/node_modules /remixapp/node_modules
-ADD package.json package-lock.json ./
-RUN npm prune --omit=dev
-
-# Build the app
-FROM base as build
-
-WORKDIR /remixapp
-
-COPY --from=deps /remixapp/node_modules /remixapp/node_modules
-
-ADD . .
-RUN npm run build
-
-# Finally, build the production image with minimal footprint
-FROM base
-
-ENV PORT="8080"
-ENV NODE_ENV="production"
-
-WORKDIR /remixapp
-
-COPY --from=production-deps /remixapp/node_modules /remixapp/node_modules
-COPY --from=build /remixapp/build /remixapp/build
-COPY --from=build /remixapp/server.mjs /remixapp/server.mjs
-COPY --from=build /remixapp/package.json /remixapp/package.json
-COPY --from=build /remixapp/start.sh /remixapp/start.sh
-
-CMD ["npm", "start"]
diff --git a/test-apps/remix-website/README.md b/test-apps/remix-website/README.md
deleted file mode 100644
index 07915e4..0000000
--- a/test-apps/remix-website/README.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# Remix Website
-
-## Setup
-
-First setup your `.env` file, use `.env.example` to know what to set.
-
-```sh
-cp .env.example .env
-```
-
-Install dependencies
-
-```sh
-npm i
-```
-
-## Local Development
-
-Now you should be good to go:
-
-```sh
-npm run dev
-```
-
-To preview local changes to the `docs` folder in the Remix repo, select "local" from the version dropdown menu on the site. Make sure you have the [remix repo](https://github.com/remix-run/remix) cloned locally and `LOCAL_REPO_RELATIVE_PATH` is pointed to the correct filepath.
-
-We leverage a number of LRUCache's to server-side cache various resources, such as processed markdown from GitHub, that expire at various times (usually after 5 minutes). If you want them to expire immediately for local development, set the `NO_CACHE` environment variable.
-
-```sh
-NO_CACHE=1 npm run dev
-```
-
-Note that by default this assumes the relative path to your local copy of the Remix docs is `../remix/docs`. This can be configured via `LOCAL_REPO_RELATIVE_PATH` in your `.env` file.
-
-## Preview
-
-To preview the production build locally:
-
-```sh
-npm run build
-npm run preview
-```
-
-## Deployment
-
-The production server is always in sync with `main`
-
-```sh
-git push origin main
-open https://remix.run
-```
-
-Pushing the "stage" tag will deploy to [staging](https://remixdotrunstage.fly.dev/blog).
-
-```sh
-git checkout my/branch
-
-# moves the `stage` tag and pushes it, triggering a deploy
-npm run push:stage
-```
-
-When you're happy with it, merge your branch into `main` and push.
-
-## Content
-
-### Authoring Blog Articles
-
-- Put a markdown file in `data/posts/{your-post-slug}.md`
-- Follow the conventions found in other blog articles for author/meta
- - Make sure your author name in the post is the same as the one in `data/authors.yml`
- - If you don't have an author photo yet, create one ([the template is in Figma](https://www.figma.com/file/6G68ZVNbR6bMHl2p8727xi/www.remix.run?node-id=6%3A2))
-- Create and optimize any inline blog post image(s) and put them in `/public/blog-images/posts/{your-post-slug}/{image-name}.{format}`
- - @TODO convention for ensuring images are large enough for 1x/2x?
-- Create a featured image for the post that shows up on the blog’s index page as well as at the top of each post. Put it in `/public/blog-images/headers/{your-post-slug}.{format}` (this gets referenced in the YAML front-matter for each post).
- - @TODO what is, or should be, the difference between this image and the social share image?
-
-When linking to other posts use `[name](article-slug)`, you don't need to do `[name](/blog/article-slug)`
-
-### Adding to Showcase
-
-[The Showcase](https://remix.run/showcase) page is a collection of sites built on Remix that are particularly impressive. If you think there is something missing from this list, feel free to open a PR for the team to review.
-
-- Record quick demo of the website (~6s)
-- Grab or screenshot the first frame of the video
-- Add both resources to public/showcase-assets
-- Run `cd public/showcase-assets && ./convert.sh` to convert images and videos to compressed formats
- - Warning: this script was created hastily with ChatGPT and little concern for others running it. Feel free to offer improvements to the script and/or documentation
-- Remove/don't commit the original video and image
-- Add new showcase example data to [showcase.yaml](./data/showcase.yaml)
-
-### Adding to Resources
-
-[The Resources](https://remix.run/resources) page is a collection of templates and libraries that make building sites with Remix even easier. For now this data is driven by a yaml file, but eventually we anticipate replacing it with a database.
-
-If you would like to contribute a new resource to these pages, simply add it to [resources.yaml](./data/resources.yaml)
-
-## CSS Notes
-
-You'll want the [tailwind VSCode plugin](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) fer sure, the hints are amazing.
-
-The color scheme has various shades but we also have a special "brand" rule for each of our brand colors so we don't have to know the specific number of that color like this: `
`.
-
-We want to use Tailwind's default classes as much as possible to avoid a large CSS file. A few things you can do to keep the styles shared:
-
-- Avoid changing anything but the theme in `tailwind.config.js`, no special classes, etc.
-- Avoid "inline rules" like `color-[#ccc]` as much as possible.
-- Silly HTML (like a wrapper div to add padding on a container) is better than one-off css rules.
-
-## Algolia Search
-
-We use [DocSearch](https://docsearch.algolia.com/) by Algolia for our documentation's search. The site is automatically scraped and indexed weekly by the [Algolia Crawler](https://crawler.algolia.com/).
-
-If the doc search results ever seem outdated or incorrect be sure to check that the crawler isn't blocked. If it is, it might just need to be canceled and restarted to kick things off again. There is also an editor in the Crawler admin that lets you adjust the crawler's script if needed.
diff --git a/test-apps/remix-website/_redirects b/test-apps/remix-website/_redirects
deleted file mode 100644
index 8fc3cc4..0000000
--- a/test-apps/remix-website/_redirects
+++ /dev/null
@@ -1,59 +0,0 @@
-/login https://remix-run.web.app/login 302
-/dashboard https://remix-run.web.app/dashboard 302
-/support https://remix-run.web.app/support 302
-/features / 302
-/api /api/conventions 302
-/podcast https://remix.transistor.fm/ 302
-/stacks https://remix.run/pages/stacks 302
-
-# V2 docs reorganization
-/docs/file-conventions/route-files-v2 /docs/file-conventions/routes 302
-/docs/file-conventions/routes-files /docs/file-conventions/routes 302
-/docs/guides/optimistic-ui /docs/discussion/pending-ui 302
-/docs/guides/routing /docs/discussion/routes 302
-/docs/guides/styling /docs/styling/bundling 302
-/docs/hooks/use-transition /docs/hooks/use-navigation 302
-/docs/other-api/asset-imports /docs/file-conventions/asset-imports 302
-/docs/other-api/dev-v2 /docs/other-api/dev 302
-/docs/other-api/fetch /docs/ 302
-/docs/other-api/react-router /docs/discussion/react-router 302
-/docs/pages/api-development-strategy /docs/start/future-flags 302
-/docs/pages/community /docs/start/community 302
-/docs/pages/contributing /docs/guides/contributing 302
-/docs/pages/faq /docs/guides/faq 302
-/docs/pages/gotchas /docs/guides/gotchas 302
-/docs/pages/philosophy /docs/discussion/introduction 302
-/docs/pages/stacks /docs/guides/templates#stacks 302
-/docs/pages/technical-explanation /docs/discussion/introduction 302
-/docs/pages/v2 /docs/start/v2 302
-/docs/route/catch-boundary /docs/route/error-boundary 302
-/docs/route/error-boundary-v2 /docs/route/error-boundary 302
-/docs/route/meta-v2 /docs/route/meta 302
-/docs/route/route-module /docs/route/action 302
-
-# The above also won't qualify for the /{slug} -> /docs/{slug} redirects since
-# they won't return anything from getRepoDoc() in routes/$.tsx, so handle the
-# root redirects as well
-/file-conventions/route-files-v2 /docs/file-conventions/routes 302
-/file-conventions/routes-files /docs/file-conventions/routes 302
-/guides/optimistic-ui /docs/discussion/pending-ui 302
-/guides/routing /docs/discussion/routes 302
-/guides/styling /docs/styling/bundling 302
-/hooks/use-transition /docs/hooks/use-navigation 302
-/other-api/asset-imports /docs/file-conventions/asset-imports 302
-/other-api/dev-v2 /docs/other-api/dev 302
-/other-api/fetch /docs/ 302
-/other-api/react-router /docs/discussion/react-router 302
-/pages/api-development-strategy /docs/start/future-flags 302
-/pages/community /docs/start/community 302
-/pages/contributing /docs/guides/contributing 302
-/pages/faq /docs/guides/faq 302
-/pages/gotchas /docs/guides/gotchas 302
-/pages/philosophy /docs/discussion/introduction 302
-/pages/stacks /docs/guides/templates#stacks 302
-/pages/technical-explanation /docs/discussion/introduction 302
-/pages/v2 /docs/start/v2 302
-/route/catch-boundary /docs/route/error-boundary 302
-/route/error-boundary-v2 /docs/route/error-boundary 302
-/route/meta-v2 /docs/route/meta 302
-/route/route-module /docs/route/action 302
diff --git a/test-apps/remix-website/app/entry.client.tsx b/test-apps/remix-website/app/entry.client.tsx
deleted file mode 100644
index 04918d0..0000000
--- a/test-apps/remix-website/app/entry.client.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import * as React from "react";
-import { hydrateRoot } from "react-dom/client";
-import { RemixBrowser } from "@remix-run/react";
-
-setTimeout(() => {
- React.startTransition(() => {
- hydrateRoot(
- document,
-
-
- ,
- );
- });
-}, 10);
diff --git a/test-apps/remix-website/app/entry.server.tsx b/test-apps/remix-website/app/entry.server.tsx
deleted file mode 100644
index c06a3e3..0000000
--- a/test-apps/remix-website/app/entry.server.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { HandleDocumentRequestFunction } from "@remix-run/node";
-import { RemixServer } from "@remix-run/react";
-import { renderToString } from "react-dom/server";
-
-let handleDocumentRequest: HandleDocumentRequestFunction = async (
- request,
- responseStatusCode,
- responseHeaders,
- remixContext,
-) => {
- const markup = renderToString(
- ,
- );
-
- responseHeaders.set("Content-Type", "text/html");
-
- return new Response("" + markup, {
- status: responseStatusCode,
- headers: responseHeaders,
- });
-};
-
-export default handleDocumentRequest;
diff --git a/test-apps/remix-website/app/env.server.ts b/test-apps/remix-website/app/env.server.ts
deleted file mode 100644
index 0679832..0000000
--- a/test-apps/remix-website/app/env.server.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { z } from "zod";
-
-const requiredInProduction: z.RefinementEffect<
- string | undefined
->["refinement"] = (value, ctx) => {
- if (process.env.NODE_ENV === "production" && !value) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "Missing required environment variable" + ctx.path.join("."),
- });
- }
-};
-
-const requiredInDevelopment: z.RefinementEffect<
- string | undefined
->["refinement"] = (value, ctx) => {
- if (process.env.NODE_ENV === "development" && !value) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "Missing required environment variable" + ctx.path.join("."),
- });
- }
-};
-
-const envSchema = z.object({
- // Get from https://app.convertkit.com/account_settings/advanced_settings
- CONVERTKIT_KEY: z.string().optional().superRefine(requiredInProduction),
-
- // A token to increase the rate limiting from 60/hr to 1000/hr
- GITHUB_TOKEN: z.string().optional().superRefine(requiredInProduction),
-
- // GitHub repo to pull docs from
- SOURCE_REPO: z.string(),
-
- // Package from which to base docs version
- RELEASE_PACKAGE: z.string(),
-
- // For development, reading the docs from a local repo
- LOCAL_REPO_RELATIVE_PATH: z
- .string()
- .optional()
- .superRefine(requiredInDevelopment),
-
- NO_CACHE: z.coerce.boolean().default(false),
-});
-
-export const env = envSchema.parse(process.env);
diff --git a/test-apps/remix-website/app/icons.svg b/test-apps/remix-website/app/icons.svg
deleted file mode 100644
index 04cda81..0000000
--- a/test-apps/remix-website/app/icons.svg
+++ /dev/null
@@ -1,124 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/test-apps/remix-website/app/lib/blog.server.ts b/test-apps/remix-website/app/lib/blog.server.ts
deleted file mode 100644
index da7f97c..0000000
--- a/test-apps/remix-website/app/lib/blog.server.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-import { DateTime } from "luxon";
-import invariant from "tiny-invariant";
-import { LRUCache } from "lru-cache";
-import yaml from "yaml";
-import { processMarkdown } from "~/lib/md.server";
-import authorsYamlFileContents from "../../data/authors.yml?raw";
-
-const postContentsBySlug = Object.fromEntries(
- Object.entries(
- import.meta.glob("../../data/posts/*.md", {
- query: "?raw",
- import: "default",
- eager: true,
- }),
- ).map(([filePath, contents]) => {
- invariant(
- typeof contents === "string",
- `Expected ${filePath} to be a string, but got ${typeof contents}`,
- );
- return [
- filePath.replace("../../data/posts/", "").replace(/\.md$/, ""),
- contents,
- ];
- }),
-);
-
-const AUTHORS: BlogAuthor[] = yaml.parse(authorsYamlFileContents);
-const AUTHOR_NAMES = AUTHORS.map((a) => a.name);
-
-const postsCache = new LRUCache({
- maxSize: 1024 * 1024 * 12, // 12 mb
- sizeCalculation(value, key) {
- return JSON.stringify(value).length + (key ? key.length : 0);
- },
-});
-
-export async function getBlogPost(slug: string): Promise {
- let cached = postsCache.get(slug);
- if (cached) return cached;
- let contents = postContentsBySlug[slug];
- if (!contents) {
- throw new Response("Not Found", { status: 404, statusText: "Not Found" });
- }
-
- let result = await processMarkdown(contents);
- let { attributes, html } = result;
- invariant(
- isMarkdownPostFrontmatter(attributes),
- `Invalid post frontmatter in ${slug}`,
- );
-
- let validatedAuthors = getValidAuthorNames(attributes.authors);
- if (validatedAuthors.length === 0) {
- console.warn(
- "The author info in `%s` is incorrect and should be fixed to match what’s in the `authors.yaml` file.",
- slug,
- );
- }
- attributes.authors = validatedAuthors;
-
- invariant(
- isMarkdownPostFrontmatter(attributes),
- `Invalid post frontmatter in ${slug}`,
- );
-
- let post: BlogPost = {
- ...attributes,
- authors: attributes.authors
- .map(getAuthor)
- .filter((a): a is BlogAuthor => !!a),
- dateDisplay: formatDate(attributes.date),
- html,
- };
- postsCache.set(slug, post);
- return post;
-}
-
-export async function getBlogPostListings(): Promise<
- Array
-> {
- let slugs = Object.keys(postContentsBySlug);
- let listings: Array = [];
- for (let slug of slugs) {
- let { html, authors, ...listing } = await getBlogPost(slug);
- if (!listing.draft) {
- listings.push({ slug, ...listing });
- }
- }
- return listings
- .sort((a, b) => b.date.getTime() - a.date.getTime())
- .map(({ date, ...listing }) => listing);
-}
-
-function getAuthor(name: string): BlogAuthor | undefined {
- return AUTHORS.find((a) => a.name === name);
-}
-
-function getValidAuthorNames(authorNames: string[]) {
- return authorNames.filter((authorName) => AUTHOR_NAMES.includes(authorName));
-}
-
-function formatDate(date: Date) {
- let offset = new Date().getTimezoneOffset();
- return (
- DateTime.fromJSDate(date)
- // Necessary to set the offset for local development
- .plus({ minutes: offset })
- .toLocaleString(DateTime.DATE_FULL, {
- locale: "en-US",
- })
- );
-}
-
-/**
- * Seems pretty easy to type up a markdown frontmatter wrong, so we've got this
- * runtime check that also gives us some type safety
- */
-function isMarkdownPostFrontmatter(obj: any): obj is MarkdownPost {
- return (
- typeof obj === "object" &&
- obj.title &&
- obj.summary &&
- obj.date instanceof Date &&
- (typeof obj.draft === "boolean" || typeof obj.draft === "undefined") &&
- (typeof obj.featured === "boolean" ||
- typeof obj.featured === "undefined") &&
- obj.image &&
- obj.imageAlt &&
- Array.isArray(obj.authors)
- );
-}
-
-interface MarkdownPostListing {
- title: string;
- slug: string;
- summary: string;
- dateDisplay: string;
- image: string;
- imageAlt: string;
- featured?: boolean;
-}
-
-/**
- * Markdown frontmatter data describing a post
- */
-interface MarkdownPost {
- title: string;
- summary: string;
- date: Date;
- dateDisplay: string;
- draft?: boolean;
- featured?: boolean;
- image: string;
- imageAlt: string;
- authors: string[];
- html: string;
-}
-
-export interface BlogAuthor {
- name: string;
- title: string;
- avatar: string;
-}
-
-export interface BlogPost extends Omit {
- authors: BlogAuthor[];
-}
diff --git a/test-apps/remix-website/app/lib/color-scheme.server.ts b/test-apps/remix-website/app/lib/color-scheme.server.ts
deleted file mode 100644
index 45003d3..0000000
--- a/test-apps/remix-website/app/lib/color-scheme.server.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { createCookie } from "@remix-run/node";
-import type { ColorScheme } from "./color-scheme";
-
-let cookie = createCookie("color-scheme", {
- maxAge: 34560000,
- sameSite: "lax",
-});
-
-export async function parseColorScheme(request: Request) {
- const header = request.headers.get("Cookie");
- const vals = await cookie.parse(header);
-
- let colorScheme = vals?.colorScheme;
- if (validateColorScheme(colorScheme)) {
- // not sure why type narrowing isn't working here
- return colorScheme;
- }
- return "system";
-}
-
-export function serializeColorScheme(colorScheme: ColorScheme) {
- let eatCookie = colorScheme === "system";
- if (eatCookie) {
- return cookie.serialize({}, { expires: new Date(0), maxAge: 0 });
- } else {
- return cookie.serialize({ colorScheme });
- }
-}
-
-export function validateColorScheme(formValue: any): formValue is ColorScheme {
- return (
- formValue === "dark" || formValue === "light" || formValue === "system"
- );
-}
diff --git a/test-apps/remix-website/app/lib/color-scheme.tsx b/test-apps/remix-website/app/lib/color-scheme.tsx
deleted file mode 100644
index 1e47dc8..0000000
--- a/test-apps/remix-website/app/lib/color-scheme.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { useMemo } from "react";
-import { useNavigation, useRouteLoaderData } from "@remix-run/react";
-import type { loader as rootLoader } from "~/root";
-import { useLayoutEffect } from "~/ui/primitives/utils";
-
-export type ColorScheme = "dark" | "light" | "system";
-
-export function useColorScheme(): ColorScheme {
- let rootLoaderData = useRouteLoaderData("root");
- let rootColorScheme = rootLoaderData?.colorScheme ?? "system";
-
- let { formData } = useNavigation();
- let optimisticColorScheme = formData?.has("colorScheme")
- ? (formData.get("colorScheme") as ColorScheme)
- : null;
- return optimisticColorScheme || rootColorScheme;
-}
-
-function syncColorScheme(media: MediaQueryList | MediaQueryListEvent) {
- if (media.matches) {
- document.documentElement.classList.add("dark");
- } else {
- document.documentElement.classList.remove("dark");
- }
-}
-
-function ColorSchemeScriptImpl() {
- let colorScheme = useColorScheme();
- // This script automatically adds the dark class to the document element if
- // colorScheme is "system" and prefers-color-scheme: dark is true.
- let script = useMemo(
- () => `
- let colorScheme = ${JSON.stringify(colorScheme)};
- if (colorScheme === "system") {
- let media = window.matchMedia("(prefers-color-scheme: dark)")
- if (media.matches) document.documentElement.classList.add("dark");
- }
- `,
- [], // eslint-disable-line -- we don't want this script to ever change
- );
-
- // Set
- useLayoutEffect(() => {
- switch (colorScheme) {
- case "light":
- document.documentElement.classList.remove("dark");
- break;
- case "dark":
- document.documentElement.classList.add("dark");
- break;
- case "system":
- let media = window.matchMedia("(prefers-color-scheme: dark)");
- syncColorScheme(media);
- media.addEventListener("change", syncColorScheme);
- return () => media.removeEventListener("change", syncColorScheme);
- default:
- console.error("Impossible color scheme state:", colorScheme);
- }
- }, [colorScheme]);
-
- // always sync the color scheme if "system" is used
- // this accounts for the docs pages adding some classnames to documentElement in root
- useLayoutEffect(() => {
- if (colorScheme === "system") {
- let media = window.matchMedia("(prefers-color-scheme: dark)");
- syncColorScheme(media);
- }
- });
-
- return ;
-}
-
-export function ColorSchemeScript({
- forceConsistentTheme,
-}: {
- forceConsistentTheme?: boolean;
-}) {
- return forceConsistentTheme ? null : ;
-}
diff --git a/test-apps/remix-website/app/lib/conf.ts b/test-apps/remix-website/app/lib/conf.ts
deleted file mode 100644
index afe0e4f..0000000
--- a/test-apps/remix-website/app/lib/conf.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-export type Speaker = {
- name: string;
- type: "emcee" | "speaker";
- linkText: string;
- link: string;
- title: string;
- imgSrc: string;
- bioHTML: string;
- slug: string;
-};
-
-export type SponsorLevel = "premier" | "gold" | "silver" | "community";
-
-export interface Sponsor {
- name: string;
- link: string;
- imgSrc: string;
- level: SponsorLevel;
- about?: string;
-}
-
-export interface Talk {
- title: string;
- description: string;
- descriptionHTML: string;
- time?: string;
- type: "regular" | "lightning" | "backup";
- speakers: Array;
-}
-
-export type ScheduleItemSpeaker = {
- slug: string;
- imgSrc: string;
- name: string;
-};
-
-export type SimpleScheduleItemRaw = {
- time: string;
- title: string;
- content: string;
- speakers?: Array;
-};
-
-export type TalkScheduleItemRaw = {
- time: string;
- talk: string;
-};
-
-type ScheduleItemRaw = SimpleScheduleItemRaw | TalkScheduleItemRaw;
-
-export type ScheduleItem = {
- time: string;
- titleHTML: string;
- contentHTML: string;
- speakers?: Array;
-};
-
-export function isSpeaker(obj: any): obj is Speaker {
- return (
- obj &&
- typeof obj === "object" &&
- obj.name &&
- obj.title &&
- obj.imgSrc &&
- obj.linkText &&
- obj.link &&
- obj.bioHTML &&
- ["emcee", "speaker"].includes(obj.type)
- );
-}
-
-export function isSponsor(obj: unknown): obj is Sponsor {
- return !!(
- obj &&
- typeof obj === "object" &&
- "name" in obj &&
- typeof obj.name === "string" &&
- "link" in obj &&
- typeof obj.link === "string" &&
- "imgSrc" in obj &&
- typeof obj.imgSrc === "string" &&
- (!("about" in obj) || typeof obj.about === "string") &&
- "level" in obj &&
- ["premier", "gold", "silver", "community"].includes(obj.level as string)
- );
-}
-
-export function isTalk(obj: any): obj is Talk {
- return (
- obj &&
- typeof obj === "object" &&
- obj.title &&
- obj.description &&
- Array.isArray(obj.speakers) &&
- ["regular", "lightning", "backup"].includes(obj.type) &&
- (!obj.time || typeof obj.time === "string") &&
- obj.speakers.every((item: any) => typeof item === "string")
- );
-}
-
-export function isSimpleScheduleItemRaw(
- obj: any,
-): obj is SimpleScheduleItemRaw {
- return (
- obj &&
- typeof obj === "object" &&
- typeof obj.time === "string" &&
- typeof obj.title === "string" &&
- typeof obj.content === "string" &&
- (!obj.speakers ||
- (Array.isArray(obj.speakers) &&
- obj.speakers.every((item: any) => typeof item === "string")))
- );
-}
-
-export function isTalkScheduleItemRaw(obj: any): obj is TalkScheduleItemRaw {
- return (
- obj &&
- typeof obj === "object" &&
- typeof obj.time === "string" &&
- typeof obj.talk === "string"
- );
-}
-
-export function isScheduleItemRaw(obj: any): obj is ScheduleItemRaw {
- return isSimpleScheduleItemRaw(obj) || isTalkScheduleItemRaw(obj);
-}
-
-export function isScheduleItemRawWithSpeakers(
- obj: any,
-): obj is SimpleScheduleItemRaw & { speakers: Array } {
- return (
- isSimpleScheduleItemRaw(obj) &&
- Array.isArray(obj.speakers) &&
- obj.speakers.every((s: any) => typeof s === "string")
- );
-}
-
-export function isScheduleItem(obj: any): obj is ScheduleItem {
- return (
- obj &&
- typeof obj === "object" &&
- typeof obj.time === "string" &&
- typeof obj.title === "string" &&
- typeof obj.content === "string" &&
- (obj.speakers
- ? Array.isArray(obj.speakers) &&
- obj.speakers.every(
- (s: any) =>
- s &&
- typeof s === "object" &&
- typeof s.slug === "string" &&
- typeof s.imgSrc === "string" &&
- typeof s.name === "string",
- )
- : true)
- );
-}
-
-export function isSpeakerArray(arr: any): arr is Array {
- return Array.isArray(arr) && arr.every(isSpeaker);
-}
-
-export function isSponsorArray(arr: any): arr is Array {
- return Array.isArray(arr) && arr.every(isSponsor);
-}
-
-export function isTalkArray(arr: any): arr is Array {
- return Array.isArray(arr) && arr.every(isTalk);
-}
-
-export function isScheduleItemArray(arr: any): arr is Array {
- return Array.isArray(arr) && arr.every(isScheduleItem);
-}
diff --git a/test-apps/remix-website/app/lib/conf2022.server.ts b/test-apps/remix-website/app/lib/conf2022.server.ts
deleted file mode 100644
index 1149259..0000000
--- a/test-apps/remix-website/app/lib/conf2022.server.ts
+++ /dev/null
@@ -1,212 +0,0 @@
-import invariant from "tiny-invariant";
-import { processMarkdown } from "~/lib/md.server";
-
-import yaml from "yaml";
-import { LRUCache } from "lru-cache";
-import type {
- ScheduleItem,
- ScheduleItemSpeaker,
- Speaker,
- Sponsor,
- Talk,
-} from "./conf";
-import {
- isScheduleItemArray,
- isScheduleItemRaw,
- isScheduleItemRawWithSpeakers,
- isSimpleScheduleItemRaw,
- isSpeaker,
- isSpeakerArray,
- isSponsor,
- isSponsorArray,
- isTalk,
- isTalkArray,
- isTalkScheduleItemRaw,
-} from "./conf";
-
-import speakersYamlFileContents from "../../data/conf/2022/speakers.yaml?raw";
-import sponsorsYamlFileContents from "../../data/conf/2022/sponsors.yaml?raw";
-import talksYamlFileContents from "../../data/conf/2022/talks.yaml?raw";
-import scheduleYamlFileContents from "../../data/conf/2022/schedule.yaml?raw";
-import { slugify } from "~/ui/primitives/utils";
-
-let cache = new LRUCache<
- string,
- Array | Array | Array | Array
->({
- max: 250,
- maxSize: 1024 * 1024 * 12, // 12 mb
- sizeCalculation(value, key) {
- return JSON.stringify(value).length + (key ? key.length : 0);
- },
-});
-
-export async function getSpeakers() {
- let cached = cache.get("speakers");
- if (isSpeakerArray(cached)) {
- return cached;
- }
-
- let speakersRaw = yaml.parse(speakersYamlFileContents);
- let speakers: Array = [];
- for (let speakerRaw of speakersRaw) {
- let { html: bioHTML } = await processMarkdown(speakerRaw.bio);
- let speakerRawWithDefaults = {
- bioHTML,
- type: "speaker",
- slug: slugify(speakerRaw.name),
- ...speakerRaw,
- };
- invariant(
- isSpeaker(speakerRawWithDefaults),
- `Speaker ${JSON.stringify(
- speakerRaw,
- )} is not valid. Please check the speakers file.`,
- );
- speakers.push({
- ...speakerRawWithDefaults,
- });
- }
- cache.set("speakers", speakers);
-
- return speakers;
-}
-
-export async function getSponsors() {
- let cached = cache.get("sponsors");
- if (isSponsorArray(cached)) {
- return cached;
- }
-
- let sponsorsRaw = yaml.parse(sponsorsYamlFileContents);
- let sponsors: Array = [];
- for (let sponsorRaw of sponsorsRaw) {
- invariant(
- isSponsor(sponsorRaw),
- `Sponsor ${JSON.stringify(
- sponsorRaw,
- )} is not valid. Please check the sponsors file.`,
- );
- sponsors.push(sponsorRaw);
- }
- cache.set("sponsors", sponsors);
-
- return sponsors;
-}
-
-export async function getTalks() {
- let cached = cache.get("talks");
- if (isTalkArray(cached)) {
- return cached;
- }
-
- let talksRaw = yaml.parse(talksYamlFileContents);
- let talks: Array = [];
- for (let talkRaw of talksRaw) {
- invariant(
- isTalk(talkRaw),
- `Talk ${JSON.stringify(
- talkRaw,
- )} is not valid. Please check the talks file.`,
- );
- let { html: descriptionHTML } = await processMarkdown(talkRaw.description);
- talks.push({
- ...talkRaw,
- descriptionHTML,
- });
- }
- cache.set("talks", talks);
-
- return talks;
-}
-
-export async function getSchedule() {
- let cached = cache.get("schedule");
- if (isScheduleItemArray(cached)) {
- return cached;
- }
-
- let allTalks = await getTalks();
- let allSpeakers = await getSpeakers();
-
- let scheduleItemsRaw = yaml.parse(scheduleYamlFileContents);
- let scheduleItems: Array = [];
-
- for (let scheduleItemRaw of scheduleItemsRaw) {
- function getSpeakersByName(speakers: Array) {
- return speakers.map((s) => {
- let speaker = allSpeakers.find((speaker) => speaker.name === s);
- invariant(
- speaker,
- `Speaker ${s} is not valid in ${JSON.stringify(
- scheduleItemRaw,
- )}. Please check the schedules file.`,
- );
- return {
- slug: speaker.slug,
- name: speaker.name,
- imgSrc: speaker.imgSrc,
- };
- });
- }
- invariant(
- isScheduleItemRaw(scheduleItemRaw),
- `schedule item ${JSON.stringify(
- scheduleItemRaw,
- )} is not valid. Please check the schedules file.`,
- );
- if (isSimpleScheduleItemRaw(scheduleItemRaw)) {
- let speakers: Array = [];
- if (isScheduleItemRawWithSpeakers(scheduleItemRaw)) {
- speakers = getSpeakersByName(scheduleItemRaw.speakers);
- }
- let [{ html: titleHTML }, { html: contentHTML }] = await Promise.all([
- processMarkdown(scheduleItemRaw.title),
- processMarkdown(scheduleItemRaw.content),
- ]);
- scheduleItems.push({
- time: scheduleItemRaw.time,
- titleHTML,
- contentHTML,
- speakers,
- });
- } else if (isTalkScheduleItemRaw(scheduleItemRaw)) {
- let talk = allTalks.find((talk) => talk.title === scheduleItemRaw.talk);
- invariant(
- talk,
- `schedule item ${JSON.stringify(scheduleItemRaw)} references talk ${
- scheduleItemRaw.talk
- } which does not exist.`,
- );
- invariant(
- talk.time === scheduleItemRaw.time,
- `Talk time is set to "${
- talk.time
- }" but that is not the time that is set in the scheduled item (${
- scheduleItemRaw.time
- }) for ${JSON.stringify(scheduleItemRaw)}`,
- );
- let [{ html: titleHTML }, { html: contentHTML }] = await Promise.all([
- processMarkdown(
- talk.type === "lightning"
- ? `⚡ ${talk.title}`
- : talk.title,
- ),
- processMarkdown(
- talk.description.length > 400
- ? `${talk.description.slice(0, 297).trim()}…`
- : talk.description,
- ),
- ]);
- scheduleItems.push({
- time: scheduleItemRaw.time,
- titleHTML,
- contentHTML,
- speakers: getSpeakersByName(talk.speakers),
- });
- }
- }
- cache.set("schedules", scheduleItems);
-
- return scheduleItems;
-}
diff --git a/test-apps/remix-website/app/lib/conf2023.server.ts b/test-apps/remix-website/app/lib/conf2023.server.ts
deleted file mode 100644
index 6e76a3f..0000000
--- a/test-apps/remix-website/app/lib/conf2023.server.ts
+++ /dev/null
@@ -1,525 +0,0 @@
-import { LRUCache } from "lru-cache";
-import { DateTime } from "luxon";
-import yaml from "yaml";
-import invariant from "tiny-invariant";
-import {
- validateSessionizeSessionData,
- validateSessionizeSpeakerData,
-} from "./conf2023";
-import sponsorsYamlFileContents from "../../data/conf/2023/sponsors.yaml?raw";
-import type {
- Speaker,
- SpeakerSession,
- Schedule,
- ScheduleSession,
- SessionizeSpeakerData,
- SessionizeSessionData,
-} from "./conf2023";
-import type { Sponsor } from "./conf";
-import { isSponsor, isSponsorArray } from "./conf";
-import { slugify } from "~/ui/primitives/utils";
-
-const CONF_TIME_ZONE = "America/Denver";
-const NO_CACHE =
- process.env.NO_CACHE != null ? process.env.NO_CACHE === "true" : undefined;
-const SPEAKERS_CACHE_KEY = "speakers";
-const SESSIONS_CACHE_KEY = "sessions";
-const SCHEDULES_CACHE_KEY = "schedules";
-const SPONSORS_CACHE_KEY = "schedules";
-const SESSIONIZE_ENDPOINT = "https://sessionize.com/api/v2/s8ds2hnu";
-const SESSIONIZE_API_DETAILS_URL =
- "https://sessionize.com/app/organizer/schedule/api/endpoint/9617/7818";
-
-let cache = new LRUCache<
- "speakers" | "sessions" | "schedules",
- (Speaker | SpeakerSession | Schedule | Sponsor)[]
->({
- max: 250,
- maxSize: 1024 * 1024 * 12, // 12 mb
- ttl: 1000 * 60 * 60 * 24, // 24 hours
- sizeCalculation(value, key) {
- return JSON.stringify(value).length + (key ? key.length : 0);
- },
-});
-
-export async function getSpeakers(
- opts: { noCache?: boolean } = {},
-): Promise {
- let { noCache = NO_CACHE ?? false } = opts;
- if (!noCache) {
- let cached = cache.get(SPEAKERS_CACHE_KEY);
- if (cached) {
- return cached as Speaker[];
- }
- }
-
- let fetch = noCache ? fetchNoCache : fetchNaiveStaleWhileRevalidate;
- let fetched = await fetch(`${SESSIONIZE_ENDPOINT}/view/Speakers`, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- });
- if (!fetched.ok) {
- throw new Error(
- "Error fetching speakers, responded with status: " + fetched.status,
- );
- }
- let json: unknown = await fetched.json();
- if (!json || !Array.isArray(json)) {
- throw new Error(
- "Error fetching speakers. Expected an array, received:\n\n" + json,
- );
- }
-
- let speakers = json
- .map((speaker: unknown) => {
- try {
- validateSessionizeSpeakerData(speaker);
- } catch (error) {
- console.warn(
- `Invalid speaker object; skipping.\n\nSee API settings to ensure expected data is included: ${SESSIONIZE_API_DETAILS_URL}\n\n`,
- "Received:\n",
- speaker,
- );
- return null;
- }
- return modelSpeaker(speaker);
- })
- .filter(isNotEmpty);
-
- if (!noCache) {
- cache.set(SPEAKERS_CACHE_KEY, speakers);
- }
- return speakers;
-}
-
-export async function getSpeakerBySlug(
- slug: string,
- opts?: { noCache?: boolean },
-): Promise {
- // Unfortunately, Sessionize doesn't have an API for fetching a single speaker,
- // so we have to fetch all of them and then filter down to the one we want.
- let speakers = await getSpeakers(opts);
- let speaker = speakers.find((s) => s.slug === slug);
- return speaker || null;
-}
-
-export async function getConfSessions(
- opts: { noCache?: boolean } = {},
-): Promise {
- let { noCache = NO_CACHE ?? false } = opts;
- if (!noCache) {
- let cached = cache.get(SESSIONS_CACHE_KEY);
- if (cached) {
- return cached as SpeakerSession[];
- }
- }
-
- let fetch = noCache ? fetchNoCache : fetchNaiveStaleWhileRevalidate;
- let fetched = await fetch(`${SESSIONIZE_ENDPOINT}/view/Sessions`, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- });
- if (!fetched.ok) {
- throw new Error(
- "Error fetching speakers, responded with status: " + fetched.status,
- );
- }
- let json: unknown = await fetched.json();
- if (!json || !Array.isArray(json)) {
- throw new Error(
- "Error fetching speakers. Expected an array, received:\n\n" + json,
- );
- }
-
- let sessions = json
- .flatMap((sessionGroup: unknown) => {
- try {
- if (
- !sessionGroup ||
- typeof sessionGroup !== "object" ||
- !("sessions" in sessionGroup) ||
- !Array.isArray(sessionGroup.sessions)
- ) {
- throw null;
- }
-
- let flatSessions = new Map();
- for (let session of sessionGroup.sessions) {
- validateSessionizeSessionData(session);
- if (flatSessions.has(session.id)) continue;
- flatSessions.set(session.id, modelSpeakerSession(session));
- }
- return Array.from(flatSessions.values());
- } catch (error) {
- return null;
- }
- })
- .filter(isNotEmpty);
-
- if (!noCache) {
- cache.set(SESSIONS_CACHE_KEY, sessions);
- }
- return sessions;
-}
-
-export async function getSchedules(
- opts: { noCache?: boolean } = {},
-): Promise {
- let { noCache = NO_CACHE ?? false } = opts;
- if (!noCache) {
- let cached = cache.get(SCHEDULES_CACHE_KEY);
- if (cached) {
- return cached as Schedule[];
- }
- }
-
- let [fetched, speakers] = await Promise.all([
- fetch(`${SESSIONIZE_ENDPOINT}/view/GridSmart`, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- }),
- getSpeakers(),
- ]);
- if (!fetched.ok) {
- throw new Error(
- "Error fetching schedule, responded with status: " + fetched.status,
- );
- }
- let json = await fetched.json();
- if (!json || !Array.isArray(json)) {
- throw new Error(
- "Error fetching schedule. Expected an array, received:\n\n" + json,
- );
- }
-
- let schedules = json
- .map((dailySchedule: unknown) => {
- try {
- if (
- !dailySchedule ||
- typeof dailySchedule !== "object" ||
- !("date" in dailySchedule) ||
- typeof dailySchedule.date !== "string" ||
- !("rooms" in dailySchedule) ||
- !("timeSlots" in dailySchedule) ||
- !Array.isArray(dailySchedule.rooms) ||
- !Array.isArray(dailySchedule.timeSlots)
- ) {
- throw null;
- }
-
- let schedule: Schedule = {
- date: getDateTime(dailySchedule.date),
- sessions: dailySchedule.timeSlots
- .flatMap(
- (timeSlot: {
- rooms: Array<{
- name: string;
- session: {
- id: string;
- title: string;
- description: string | null;
- startsAt: string;
- endsAt: string;
- speakers: Array<{
- id: string;
- name: string;
- }>;
- };
- }>;
- }) => {
- return timeSlot.rooms.flatMap((room) => {
- let session = room.session;
- let sessionSpeakers = session.speakers.map((speaker) => {
- let found = speakers.find((s) => s.id === speaker.id)!;
- let sessionSpeaker: ScheduleSession["speakers"][number] = {
- id: speaker.id,
- slug: found.slug,
- nameFirst: found.nameFirst,
- nameLast: found.nameLast,
- nameFull: found.nameFull,
- imgUrl: found.imgUrl,
- };
- return sessionSpeaker;
- });
- return {
- id: session.id,
- room: room.name,
- title: session.title.startsWith("Registration")
- ? "Registration"
- : session.title,
- description: session.description,
- startsAt: getDateTime(session.startsAt),
- endsAt: getDateTime(session.endsAt),
- speakers: sessionSpeakers,
- };
- });
- },
- )
- .sort((a, b) => {
- let isEariler = a.startsAt < b.startsAt;
- let isLater = a.startsAt > b.startsAt;
- let isRegistration =
- a.title === "Registration" && b.title !== "Registration";
- return isEariler ? -1 : isLater ? 1 : isRegistration ? -1 : 0;
- }),
- };
-
- return schedule;
-
- // let flatSessions = new Map();
- // for (let session of scheduleGroup.sessions) {
- // validateSessionizeSessionData(session);
- // if (flatSessions.has(session.id)) continue;
- // flatSessions.set(session.id, modelSpeakerSession(session));
- // }
- // return Array.from(flatSessions.values());
- } catch (error) {
- return null;
- }
- })
- .filter(isNotEmpty)
- .sort((a, b) => {
- let isEariler = a.date < b.date;
- let isLater = a.date > b.date;
- return isEariler ? -1 : isLater ? 1 : 0;
- });
-
- if (!noCache) {
- cache.set(SCHEDULES_CACHE_KEY, schedules);
- }
-
- return schedules;
-}
-
-function getDateTime(isoDate: string) {
- return DateTime.fromISO(isoDate, { zone: CONF_TIME_ZONE });
-}
-
-export function formatDate(
- date: DateTime,
- opts: Intl.DateTimeFormatOptions,
-): string {
- return (
- date
- // .plus({ minutes: new Date().getTimezoneOffset() })
- .toLocaleString(opts, { locale: "en-US" })
- );
-}
-
-function modelSpeaker(speaker: SessionizeSpeakerData): Speaker {
- let id = String(speaker.id);
- let { nameFirst, nameLast, nameFull } = getSpeakerNames(speaker);
-
- let tagLine = getSpeakerTagLine(speaker);
- let imgUrl = speaker.profilePicture ? String(speaker.profilePicture) : null;
- let twitterUrl = speaker.links?.find((link) => link.title === "Twitter")?.url;
- let twitterHandle = twitterUrl?.includes("twitter.com")
- ? "@" + getTwitterHandle(twitterUrl)
- : null;
- let bio = speaker.bio
- ? speaker.bio
- .split(/[\r\n]+/g)
- .map((line) => line.trim())
- .filter(Boolean)
- .join("\n")
- : null;
- let isEmcee =
- speaker.questionAnswers?.find((qa) => qa.question === "Emcee?")?.answer ===
- "true";
-
- let validatedSpeaker: Speaker = {
- id,
- tagLine,
- bio,
- nameFirst,
- nameLast,
- nameFull,
- slug: slugify(nameFull),
- imgUrl,
- twitterHandle,
- isTopSpeaker: !!speaker.isTopSpeaker,
- isEmcee,
- links: speaker.links || [],
- };
- return validatedSpeaker;
-}
-
-function modelSpeakerSession(session: SessionizeSessionData): SpeakerSession {
- let id = String(session.id);
- let title = String(session.title).trim();
- let description = session.description
- ? String(session.description).trim()
- : null;
- let startsAt = session.startsAt ? getDateTime(session.startsAt) : null;
- let endsAt = session.endsAt ? getDateTime(session.endsAt) : null;
- let speakers =
- session.speakers && Array.isArray(session.speakers)
- ? session.speakers.map((speaker) => {
- return {
- id: speaker.id,
- name: speaker.name,
- slug: slugify(speaker.name),
- };
- })
- : [];
- return {
- id,
- title,
- description,
- startsAt,
- endsAt,
- speakers,
- };
-}
-
-function getSpeakerNames(speaker: SessionizeSpeakerData) {
- let preferredName = speaker.questionAnswers?.find(
- (qa) => qa.question === "Preferred Name",
- )?.answer;
- let nameFirst: string;
- let nameLast = speaker.lastName ? String(speaker.lastName).trim() : "";
- if (preferredName) {
- nameFirst = preferredName.includes(nameLast)
- ? preferredName.slice(0, preferredName.indexOf(nameLast)).trim()
- : preferredName.trim();
- } else {
- nameFirst = speaker.firstName ? String(speaker.firstName).trim() : "";
- }
- let nameFull = [nameFirst, nameLast].filter(Boolean).join(" ");
-
- return {
- nameFirst,
- nameLast,
- nameFull,
- preferredName,
- };
-}
-
-function getSpeakerTagLine(speaker: SessionizeSpeakerData) {
- if (speaker.tagLine) {
- return speaker.tagLine.trim();
- }
- let jobTitle: string | undefined | null;
- if (
- (jobTitle = speaker.questionAnswers?.find(
- (qa) => qa.question === "Current Job Title",
- )?.answer)
- ) {
- return jobTitle.trim();
- }
- return null;
-}
-
-function getTwitterHandle(url: string) {
- let match = url.match(/twitter\.com\/([^/]+)/);
- return match?.[1] || null;
-}
-
-function isNotEmpty(value: T | null | undefined): value is T {
- return value != null;
-}
-
-async function fetchNoCache(url: string, opts?: RequestInit) {
- return fetch(url, {
- ...opts,
- cache: "no-cache",
- });
-}
-
-// https://developer.mozilla.org/en-US/docs/Web/API/Request/cache#examples
-async function fetchNaiveStaleWhileRevalidate(
- url: string,
- opts?: {
- method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
- headers: HeadersInit;
- },
-) {
- let method = opts?.method || "GET";
- let headers = opts?.headers || {};
- let controller = new AbortController();
- let res: Response;
- try {
- res = await fetch(url, {
- method,
- headers,
- cache: "only-if-cached",
- signal: controller.signal,
- });
- } catch (err) {
- // Workaround for Chrome, which fails with a TypeError
- if (err instanceof TypeError && err.message === "Failed to fetch") {
- return fetchWithForceCache();
- }
- throw err;
- }
- if (res.status === 504) {
- return fetchWithForceCache();
- }
-
- let date = res.headers.get("date");
- let dt = date ? new Date(date).getTime() : 0;
- if (dt < Date.now() - 60 * 60 * 24) {
- // If older than 24 hours
- controller.abort();
- controller = new AbortController();
- return fetch(url, {
- method,
- headers,
- cache: "reload",
- signal: controller.signal,
- });
- }
-
- if (dt < Date.now() - 60 * 60 * 24 * 7) {
- // If it's older than 1 week, fetch but don't wait for it. We'll return the
- // stale value while this call "revalidates"
- fetch(url, {
- method,
- headers,
- cache: "no-cache",
- });
- }
-
- // return possibly stale value
- return res;
-
- function fetchWithForceCache() {
- controller.abort();
- controller = new AbortController();
- return fetch(url, {
- method,
- headers,
- cache: "force-cache",
- signal: controller.signal,
- });
- }
-}
-
-export async function getSponsors() {
- let cached = cache.get(SPONSORS_CACHE_KEY);
- if (isSponsorArray(cached)) {
- return cached;
- }
-
- let sponsorsRaw = yaml.parse(sponsorsYamlFileContents);
- let sponsors: Array = [];
- for (let sponsorRaw of sponsorsRaw) {
- invariant(
- isSponsor(sponsorRaw),
- `Sponsor ${JSON.stringify(
- sponsorRaw,
- )} is not valid. Please check the sponsors file.`,
- );
- sponsors.push(sponsorRaw);
- }
- cache.set(SPONSORS_CACHE_KEY, sponsors);
-
- return sponsors;
-}
diff --git a/test-apps/remix-website/app/lib/conf2023.ts b/test-apps/remix-website/app/lib/conf2023.ts
deleted file mode 100644
index 7208465..0000000
--- a/test-apps/remix-website/app/lib/conf2023.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import type { DateTime } from "luxon";
-
-export function validateSessionizeSpeakerData(
- data: unknown,
-): asserts data is SessionizeSpeakerData {
- if (
- data == null ||
- typeof data !== "object" ||
- !("id" in data) ||
- !("firstName" in data) ||
- !("lastName" in data) ||
- !("fullName" in data) ||
- !("tagLine" in data) ||
- !("links" in data) ||
- !("questionAnswers" in data) ||
- !("profilePicture" in data) ||
- !("isTopSpeaker" in data) ||
- (data.links != null && !Array.isArray(data.links)) ||
- (data.questionAnswers != null && !Array.isArray(data.questionAnswers))
- ) {
- throw new Error("Invalid speaker data");
- }
-}
-
-export function validateSessionizeSessionData(
- data: unknown,
-): asserts data is SessionizeSessionData {
- if (
- data == null ||
- typeof data !== "object" ||
- !("id" in data)
- // !("title" in data) ||
- // !("description" in data) ||
- // !("startsAt" in data) ||
- // !("endsAt" in data)
- // TODO: ...
- ) {
- throw new Error("Invalid session data");
- }
-}
-
-export interface Speaker {
- id: string;
- nameFirst: string;
- nameLast: string;
- nameFull: string;
- slug: string;
- bio: string | null;
- tagLine: string | null;
- imgUrl: string | null;
- twitterHandle: string | null;
- isTopSpeaker: boolean;
- isEmcee: boolean;
- links: Array<{
- title: string;
- linkType: "Twitter" | "LinkedIn" | "Blog" | "Company_Website";
- url: string;
- }>;
-}
-
-export interface SpeakerSession {
- id: string;
- title: string;
- description: string | null;
- startsAt: DateTime | null;
- endsAt: DateTime | null;
- speakers: Array<{ id: string; slug: string; name: string }>;
-}
-
-export interface ScheduleSession {
- id: string;
- room: string;
- title: string;
- description: string | null;
- startsAt: DateTime;
- endsAt: DateTime;
- speakers: Array<{
- id: string;
- slug: string;
- nameFirst: string | null;
- nameLast: string | null;
- nameFull: string;
- imgUrl: string | null;
- }>;
-}
-
-export interface Schedule {
- date: DateTime;
- sessions: ScheduleSession[];
-}
-
-export interface SessionizeSpeakerData {
- id: number | string;
- firstName: string | null;
- lastName: string | null;
- fullName: string | null;
- tagLine: string | null;
- bio: string | null;
- links: Array<{
- title: string;
- linkType: "Twitter" | "LinkedIn" | "Blog" | "Company_Website";
- url: string;
- }> | null;
- questionAnswers: Array<{
- question: string;
- answer: string | null;
- }> | null;
- profilePicture: string | null;
- isTopSpeaker: boolean;
-}
-
-export interface SessionizeSessionData {
- id: number | string;
- title: string;
- description: string | null;
- startsAt: string | null;
- endsAt: string | null;
- isServiceSession: boolean;
- isPlenumSession: boolean;
- speakers: Array<{ id: string; name: string }>;
- categories: Array<{
- id: string;
- name: string;
- categoryItems: Array<{ id: string; name: string }>;
- }>;
- roomId: number | null;
- room: string | null;
- status: string;
-}
diff --git a/test-apps/remix-website/app/lib/convertkit.ts b/test-apps/remix-website/app/lib/convertkit.ts
deleted file mode 100644
index 834e68e..0000000
--- a/test-apps/remix-website/app/lib/convertkit.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { env } from "~/env.server";
-
-export async function subscribeToNewsletter(email: string) {
- let TOKEN = env.CONVERTKIT_KEY;
- let URL = "https://api.convertkit.com/v3";
- let FORM_ID = "1334747";
-
- let res = await fetch(`${URL}/forms/${FORM_ID}/subscribe`, {
- method: "POST",
- body: JSON.stringify({ api_key: TOKEN, email }),
- headers: { "Content-Type": "application/json; charset=utf-8" },
- });
-
- let data = await res.json();
- if (data.error) {
- throw new Error(data.error);
- }
-}
diff --git a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/components/index.md b/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/components/index.md
deleted file mode 100644
index 68f4f8a..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/components/index.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Components
-order: 1
----
diff --git a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/components/link.md b/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/components/link.md
deleted file mode 100644
index aaba370..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/components/link.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-title: Link
----
-
-# Link
-
-This is a link document.
diff --git a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/components/router.md b/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/components/router.md
deleted file mode 100644
index cf186c4..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/components/router.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-title: Router
----
-
-# Router
-
-This is a router document.
diff --git a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/index.md b/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/index.md
deleted file mode 100644
index 176166f..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/index.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-title: Fixture Index
----
-
-# Welcome
-
-You are so very welcome.
diff --git a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/pages/index.md b/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/pages/index.md
deleted file mode 100644
index da761cb..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/pages/index.md
+++ /dev/null
@@ -1,3 +0,0 @@
----
-title: Pages
----
diff --git a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/pages/overview.md b/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/pages/overview.md
deleted file mode 100644
index 86bfe89..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/pages/overview.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Overview
----
-
-# API Overview
diff --git a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/pages/tutorial.md b/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/pages/tutorial.md
deleted file mode 100644
index 2e508c5..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/__fixture__/docs/pages/tutorial.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Quickstart Tutorial
-order: 1
----
-
-# Quickstart Blog Tutorial
-
-This is a tutorial
diff --git a/test-apps/remix-website/app/lib/gh-docs/branches.ts b/test-apps/remix-website/app/lib/gh-docs/branches.ts
deleted file mode 100644
index 2423f1e..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/branches.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { LRUCache } from "lru-cache";
-import type { Octokit } from "octokit";
-
-type CacheContext = { octokit: Octokit };
-
-declare global {
- var branchesCache: LRUCache;
-}
-
-export async function getBranches(repo: string, { octokit }: CacheContext) {
- return branchesCache.fetch(repo, {
- context: { octokit },
- });
-}
-
-global.branchesCache ??= new LRUCache({
- max: 3,
- ttl: 1000 * 60 * 5, // 5 minutes, so we can see new tags quickly
- allowStale: true,
- noDeleteOnFetchRejection: true,
- fetchMethod: fetchBranches,
-});
-
-async function fetchBranches(
- key: string,
- staleValue: string[] | undefined,
- { context }: LRUCache.FetchOptionsWithContext,
-) {
- console.log("Fetching fresh branches", key);
- let [owner, repo] = key.split("/");
- let { data } = await context.octokit.rest.repos.listBranches({
- mediaType: { format: "json" },
- owner,
- repo,
- per_page: 100,
- });
- return data.map((branch: { name: string }) => branch.name);
-}
diff --git a/test-apps/remix-website/app/lib/gh-docs/docs.test.ts b/test-apps/remix-website/app/lib/gh-docs/docs.test.ts
deleted file mode 100644
index 2664529..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/docs.test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import path from "path";
-import fs from "fs";
-import tar from "tar";
-import { getMenuFromStream } from "./docs";
-
-describe("getMenuFromStream", () => {
- it("sorts the menu with children and stuff", async () => {
- let stream = await getFixtureStream();
- let menu = await getMenuFromStream(stream);
-
- // removes `index.md` so only 2, not 3
- expect(menu.length).toBe(2);
- expect(menu[0].attrs.title).toBe("Components");
- expect(menu[0].children.length).toBe(2);
-
- expect(menu[1].attrs.title).toBe("Pages");
- expect(menu[1].children.length).toBe(2);
-
- expect(menu[1].children.length).toBe(2);
- expect(menu[1].slug).toBe("pages");
- expect(menu[1].children[0].attrs.title).toBe("Quickstart Tutorial");
- expect(menu[1].children[0].slug).toBe("pages/tutorial");
- });
-});
-
-async function getFixtureStream(): Promise {
- let fixturePath = path.join(__dirname, "__fixture__");
- let writePath = path.join(fixturePath, "tar.tgz");
- await tar.c({ gzip: true, file: writePath }, [fixturePath]);
- return fs.createReadStream(writePath);
-}
diff --git a/test-apps/remix-website/app/lib/gh-docs/docs.ts b/test-apps/remix-website/app/lib/gh-docs/docs.ts
deleted file mode 100644
index b229d89..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/docs.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-import { processMarkdown } from "~/lib/md.server";
-import { LRUCache } from "lru-cache";
-import parseYamlHeader from "gray-matter";
-import { getRepoContent } from "./repo-content";
-import { getRepoTarballStream } from "./repo-tarball";
-import { createTarFileProcessor } from "./tarball.server";
-import { load as $ } from "cheerio";
-import { env } from "~/env.server";
-
-interface MenuDocAttributes {
- title: string;
- order?: number;
- new?: boolean;
- [key: string]: any;
-}
-
-export interface MenuDoc {
- attrs: MenuDocAttributes;
- children: MenuDoc[];
- filename: string;
- hasContent: boolean;
- slug: string;
-}
-
-export interface Doc extends Omit {
- html: string;
- headings: { html: string | null; slug: string | undefined }[];
-}
-
-declare global {
- var menuCache: LRUCache;
- var docCache: LRUCache;
-}
-
-let NO_CACHE = env.NO_CACHE;
-
-global.menuCache ??= new LRUCache({
- // let menuCache = new LRUCache({
- max: 10,
- ttl: NO_CACHE ? 1 : 300000, // 5 minutes
- allowStale: !NO_CACHE,
- noDeleteOnFetchRejection: true,
- fetchMethod: fetchMenu,
-});
-
-async function fetchMenu(key: string) {
- console.log(`Fetching fresh menu: ${key}`);
- let [repo, ref] = key.split(":");
- let stream = await getRepoTarballStream(repo, ref);
- let menu = await getMenuFromStream(stream);
- return menu;
-}
-
-export async function getMenu(
- repo: string,
- ref: string,
- lang: string,
-): Promise {
- let menu = await menuCache.fetch(`${repo}:${ref}`);
- return menu || undefined;
-}
-
-function parseAttrs(
- md: string,
- filename: string,
-): { content: string; attrs: Doc["attrs"] } {
- let { data, content } = parseYamlHeader(md);
- return {
- content,
- attrs: {
- title: filename,
- ...data,
- },
- };
-}
-
-/**
- * While we're using HTTP caching, we have this memory cache too so that
- * document requests and data request to the same document can do less work for
- * new versions. This makes our origin server very fast, but adding HTTP caching
- * let's have simpler and faster deployments with just one origin server, but
- * still distribute the documents across the CDN.
- */
-global.docCache ??= new LRUCache({
- max: 300,
- ttl: NO_CACHE ? 1 : 1000 * 60 * 5, // 5 minutes
- allowStale: !NO_CACHE,
- noDeleteOnFetchRejection: true,
- fetchMethod: fetchDoc,
-});
-
-async function fetchDoc(key: string): Promise {
- let [repo, ref, slug] = key.split(":");
- let filename = `${slug}.md`;
- let md = await getRepoContent(repo, ref, filename);
- if (md === null) {
- throw Error(`Could not find ${filename} in ${repo}@${ref}`);
- }
- let { html, attributes } = await processMarkdown(md);
- let attrs: MenuDocAttributes = { title: filename };
- if (isPlainObject(attributes)) {
- attrs = { title: filename, ...attributes };
- }
-
- // sorry, cheerio is so much easier than using rehype stuff.
- let headings = createTableOfContentsFromHeadings(html);
- return { attrs, filename, html, slug, headings, children: [] };
-}
-
-// create table of contents from h2 and h3 headings
-function createTableOfContentsFromHeadings(html: string) {
- let $headings = $(html)("h2,h3");
-
- let headings = $headings.toArray().map((heading) => ({
- html: $(heading)("a").remove().end().children().html(),
- slug: heading.attributes.find((attr) => attr.name === "id")?.value,
- }));
-
- return headings;
-}
-
-export async function getDoc(
- repo: string,
- ref: string,
- slug: string,
-): Promise {
- let key = `${repo}:${ref}:${slug}`;
- let doc = await docCache.fetch(key);
-
- return doc || undefined;
-}
-
-/**
- * Exported for unit tests
- */
-export async function getMenuFromStream(stream: NodeJS.ReadableStream) {
- let docs: MenuDoc[] = [];
- let processFiles = createTarFileProcessor(stream);
- await processFiles(async ({ filename, content }) => {
- let { attrs, content: md } = parseAttrs(content, filename);
- let slug = makeSlug(filename);
-
- // don't need docs/index.md in the menu
- if (slug === "") return;
-
- // can have docs not in the menu
- if (attrs.hidden) return;
-
- docs.push({
- attrs,
- filename,
- slug: makeSlug(filename),
- hasContent: md.length > 0,
- children: [],
- });
- });
-
- // sort so we can process parents before children
- docs.sort((a, b) => (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0));
-
- // construct the hierarchy
- let tree: MenuDoc[] = [];
- let map = new Map();
- for (let doc of docs) {
- let { slug } = doc;
-
- let parentSlug = slug.substring(0, slug.lastIndexOf("/"));
- map.set(slug, doc);
-
- if (parentSlug) {
- let parent = map.get(parentSlug);
- if (parent) {
- parent.children.push(doc);
- }
- } else {
- tree.push(doc);
- }
- }
-
- let sortDocs = (a: MenuDoc, b: MenuDoc) =>
- (a.attrs.order || Infinity) - (b.attrs.order || Infinity);
-
- // sort the parents and children
- tree.sort(sortDocs);
- for (let category of tree) {
- category.children.sort(sortDocs);
- }
-
- return tree;
-}
-
-/**
- * Removes the extension from markdown file names.
- */
-function makeSlug(docName: string): string {
- // Could be as simple as `/^docs\//` but local development tarballs have more
- // path in front of "docs/", so grab any of that stuff too. Maybe there's a
- // way to control the basename of files when we make the local tarball but I
- // dunno how to do that right now.
- return docName
- .replace(/^(.+\/)?docs\//, "")
- .replace(/\.md$/, "")
- .replace(/index$/, "")
- .replace(/\/$/, "");
-}
-
-function isPlainObject(obj: unknown): obj is Record {
- return !!obj && Object.prototype.toString.call(obj) === "[object Object]";
-}
diff --git a/test-apps/remix-website/app/lib/gh-docs/index.ts b/test-apps/remix-website/app/lib/gh-docs/index.ts
deleted file mode 100644
index 89c6f85..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/index.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { getDoc, getMenu } from "./docs";
-import { getBranches } from "./branches";
-import { getLatestVersion, getTags } from "./tags";
-import invariant from "tiny-invariant";
-import type { Octokit } from "octokit";
-import { env } from "~/env.server";
-
-export { validateParams } from "./params";
-export { getRepoTarballStream } from "./repo-tarball";
-export {
- getLatestVersion,
- getAllReleases,
- getLatestVersionHeads,
-} from "./tags";
-
-export type { Doc } from "./docs";
-
-const REPO = env.SOURCE_REPO;
-const RELEASE_PACKAGE = env.RELEASE_PACKAGE;
-
-export function getRepoTags({
- octokit,
- releasePackage,
-}: {
- octokit: Octokit;
- releasePackage: string;
-}) {
- return getTags(REPO, { octokit, releasePackage });
-}
-
-export function getRepoBranches({ octokit }: { octokit: Octokit }) {
- return getBranches(REPO, { octokit });
-}
-
-export async function getLatestRepoTag({
- octokit,
- releasePackage,
-}: {
- octokit: Octokit;
- releasePackage: string;
-}): Promise {
- let tags = await getTags(REPO, { octokit, releasePackage });
- invariant(tags, "Expected tags in getLatestRepoTag");
- return getLatestVersion(tags);
-}
-
-export function getRepoDocsMenu(ref: string, lang: string) {
- return getMenu(REPO, fixupRefName(ref), lang);
-}
-
-export function getRepoDoc(ref: string, slug: string) {
- return getDoc(REPO, fixupRefName(ref), slug);
-}
-
-function fixupRefName(ref: string) {
- return ["dev", "main", "release-next", "local"].includes(ref) ||
- // when we switched to changesets the `v` went away, so we use that as a way
- // to know if we need to add hte `remix@` prefix for interacting w/
- // github.
- ref.startsWith("v")
- ? ref
- : `${RELEASE_PACKAGE}@${ref}`;
-}
diff --git a/test-apps/remix-website/app/lib/gh-docs/params.test.ts b/test-apps/remix-website/app/lib/gh-docs/params.test.ts
deleted file mode 100644
index 8b84e0c..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/params.test.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import { validateParams as validate } from "./params";
-
-let LATEST_V1_MAJOR_TAG: string;
-let LATEST_V1_MINOR_TAG: string;
-let LATEST_STABLE_TAG: string;
-
-const TAGS = [
- "v1.0.0",
- "v1.1.0",
- (LATEST_V1_MINOR_TAG = "v1.1.1"),
- (LATEST_V1_MAJOR_TAG = "v1.2.0"),
- "v2.0.0",
- (LATEST_STABLE_TAG = "v2.1.0"),
- "v3.0.0-pre.0",
-];
-
-const BRANCHES = ["main", "dev"];
-
-describe("validateParams", () => {
- describe("with a valid lang in the first position", () => {
- describe("and a valid tag in the second position", () => {
- it("returns null", () => {
- expect(validate(TAGS, BRANCHES, { lang: "en", ref: "v1.0.0" })).toBe(
- null,
- );
- expect(
- validate(TAGS, BRANCHES, { lang: "en", ref: "v1.0.0", "*": "beef" }),
- ).toBe(null);
- });
- });
-
- describe("and a valid shorthand tag", () => {
- it("expands the major shorthand", () => {
- expect(validate(TAGS, BRANCHES, { lang: "en", ref: "v1" })).toBe(
- `en/${LATEST_V1_MAJOR_TAG}`,
- );
- });
- it("expands the minor shorthand", () => {
- expect(validate(TAGS, BRANCHES, { lang: "en", ref: "v1.1" })).toBe(
- `en/${LATEST_V1_MINOR_TAG}`,
- );
- });
- it("expands the major shorthand, preserves splat", () => {
- expect(
- validate(TAGS, BRANCHES, { lang: "en", ref: "v1", "*": "beef/taco" }),
- ).toBe(`en/${LATEST_V1_MAJOR_TAG}/beef/taco`);
- });
- });
-
- describe("and a valid branch in the second position", () => {
- it("returns null", () => {
- expect(validate(TAGS, BRANCHES, { lang: "en", ref: "main" })).toBe(
- null,
- );
- expect(
- validate(TAGS, BRANCHES, { lang: "en", ref: "main", "*": "beef" }),
- ).toBe(null);
- });
- });
-
- it("redirects to the latest stable tag", () => {
- expect(validate(TAGS, BRANCHES, { lang: "en" })).toBe(
- `en/${LATEST_STABLE_TAG}`,
- );
- });
-
- describe("and an invalid branch or tag in the second position", () => {
- it("inserts latest tag", () => {
- expect(validate(TAGS, BRANCHES, { lang: "en", ref: "beef" })).toBe(
- `en/${LATEST_STABLE_TAG}/beef`,
- );
- });
- });
- });
-
- describe("with a valid tag in the first param", () => {
- it("adds lang", () => {
- expect(validate(TAGS, BRANCHES, { lang: "v1.0.0" })).toBe("en/v1.0.0");
- });
-
- it("adds lang and keeps splat params around", () => {
- expect(validate(TAGS, BRANCHES, { lang: "v1.0.0", ref: "beef" })).toBe(
- "en/v1.0.0/beef",
- );
- });
- });
-
- describe("with a valid shorthand tag in the first param", () => {
- it("adds lang, expands the major tag", () => {
- expect(validate(TAGS, BRANCHES, { lang: "v1" })).toBe(
- `en/${LATEST_V1_MAJOR_TAG}`,
- );
- });
- });
-
- describe("with a valid branch in the first position", () => {
- it("adds the lang", () => {
- expect(validate(TAGS, BRANCHES, { lang: "main" })).toBe("en/main");
- });
- });
-
- describe("without a valid branch or tag in the first position", () => {
- it("adds lang and latest stable tag", () => {
- expect(validate(TAGS, BRANCHES, { lang: "beef" })).toBe(`en/main/beef`);
- });
- describe("without a valid branch or tag in the second position", () => {
- it("adds lang and latest stable tag", () => {
- expect(validate(TAGS, BRANCHES, { lang: "beef", ref: "cheese" })).toBe(
- `en/main/beef/cheese`,
- );
- });
- });
- });
-});
diff --git a/test-apps/remix-website/app/lib/gh-docs/params.ts b/test-apps/remix-website/app/lib/gh-docs/params.ts
deleted file mode 100644
index ccd0076..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/params.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import * as semver from "semver";
-import iso_639_1, { type LanguageCode } from "iso-639-1";
-
-const CODES = iso_639_1.getAllCodes();
-
-export function validateParams(
- tags: string[],
- branches: string[],
- params: { lang: string; ref?: string; ["*"]?: string },
- lang: string = "en",
-): string | null {
- let { lang: first, ref: second, "*": splat } = params;
-
- let firstIsLang = CODES.includes(first as LanguageCode);
- let secondIsRef =
- second && (tags.includes(second) || branches.includes(second));
-
- if (firstIsLang) {
- if (!second) {
- return `${first}/${semver.maxSatisfying(tags, "*", {
- includePrerelease: false,
- })}`;
- }
-
- if (!secondIsRef) {
- let expandedRef = semver.maxSatisfying(tags, second, {
- includePrerelease: false,
- });
- let latest = semver.maxSatisfying(tags, "*");
- let path = [first];
-
- if (expandedRef) {
- path.push(expandedRef);
- } else if (latest) {
- if (semver.valid(second)) {
- // If second looks like a semver tag but we didn't find it as a ref in
- // the repo, we might just have a stale set of branches/tags from github.
- // Instead of pushing both in and generating a 404 (/en/1.16.0/1.17.0/pages/...)
- // we just point them back to the requested doc on main
- path.push("main");
- } else {
- path.push(latest, second);
- }
- }
-
- if (splat) path.push(splat);
- return path.join("/");
- }
- }
-
- let ref =
- tags.includes(first) || branches.includes(first)
- ? first
- : semver.maxSatisfying(tags, first, { includePrerelease: false });
- if (ref) {
- let path = [lang, ref];
- if (second) path.push(second);
- if (splat) path.push(splat);
- return path.join("/");
- }
-
- // If we don't have a language and we can't detect a ref, fallback to `main`
- // so we get the latest and we can be comfortable redirecting on 404 slugs.
- // If we were to redirect to the latest semver here (i.e., 2.1.0), then we
- // can't safely process a slug 404 redirect in docs/$lang.$ref/$.tsx since we
- // don't know that they weren't looking for something in a specific version
- if (!firstIsLang && !ref) {
- let path = [lang, "main", first];
- if (second) path.push(second);
- if (splat) path.push(splat);
- return path.join("/");
- }
-
- return null;
-}
diff --git a/test-apps/remix-website/app/lib/gh-docs/repo-content.ts b/test-apps/remix-website/app/lib/gh-docs/repo-content.ts
deleted file mode 100644
index ba744dc..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/repo-content.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import fsp from "fs/promises";
-import path from "path";
-import invariant from "tiny-invariant";
-import { env } from "~/env.server";
-
-/**
- * Fetches the contents of a file in a repository or from your local disk.
- *
- * @param ref The GitHub ref, use `"local"` for local docs development
- * @param filepath The filepath inside the repo (including "docs/")
- * @returns The text of the file
- */
-export async function getRepoContent(
- repoPair: string,
- ref: string,
- filepath: string,
-): Promise {
- if (ref === "local") return getLocalContent(filepath);
- let [owner, repo] = repoPair.split("/");
- let pathname = `/${owner}/${repo}/${ref}/${filepath}`;
- let response = await fetch(
- new URL(pathname, "https://raw.githubusercontent.com/").href,
- { headers: { "User-Agent": `docs:${owner}/${repo}` } },
- );
- if (!response.ok) return null;
- return response.text();
-}
-
-/**
- * Reads a single file from your local source repository
- */
-async function getLocalContent(filepath: string): Promise {
- invariant(
- env.LOCAL_REPO_RELATIVE_PATH,
- "LOCAL_REPO_RELATIVE_PATH is not set",
- );
- let localFilePath = path.join(env.LOCAL_REPO_RELATIVE_PATH, filepath);
- let content = await fsp.readFile(localFilePath);
- return content.toString();
-}
diff --git a/test-apps/remix-website/app/lib/gh-docs/repo-tarball.ts b/test-apps/remix-website/app/lib/gh-docs/repo-tarball.ts
deleted file mode 100644
index 52227c2..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/repo-tarball.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import followRedirects from "follow-redirects";
-import fs from "fs";
-import path from "path";
-import tar from "tar";
-import invariant from "tiny-invariant";
-import { env } from "~/env.server";
-
-/**
- * Fetches a repo tarball from GitHub or your local repo as a tarball in
- * development.
- *
- * @param ref GitHub ref (main, v6.0.0, etc.) use "local" for local repo.
- * @returns The repo tarball
- */
-export async function getRepoTarballStream(
- repo: string,
- ref: string,
-): Promise {
- if (ref === "local") {
- console.log("Using local repo");
- return getLocalTarballStream();
- }
-
- let agent = new followRedirects.https.Agent({ keepAlive: true });
- let tarballURL = `https://github.com/${repo}/archive/${ref}.tar.gz`;
- let { hostname, pathname } = new URL(tarballURL);
- let options = { agent: agent, hostname: hostname, path: pathname };
-
- try {
- let res = await get(options);
- if (res.statusCode === 200) {
- return res;
- }
- throw new Error(`Could not fetch ${tarballURL}`);
- } catch (err) {
- throw new Error(`Could not fetch ${tarballURL}`);
- }
-}
-
-/**
- * Creates a tarball out of your local source repository so that the rest of the
- * code in this app can continue to work the same for local dev as in
- * production.
- */
-export async function getLocalTarballStream(): Promise {
- invariant(
- env.LOCAL_REPO_RELATIVE_PATH,
- "LOCAL_REPO_RELATIVE_PATH is not set",
- );
- let localDocsPath = path.join(
- process.cwd(),
- env.LOCAL_REPO_RELATIVE_PATH,
- "docs",
- );
- await tar.c({ gzip: true, file: ".local.tgz" }, [localDocsPath]);
- return fs.createReadStream(".local.tgz");
-}
-
-// FIXME: I don't know the types here
-function get(options: any): any {
- return new Promise((accept, reject) => {
- followRedirects.https.get(options, accept).on("error", reject);
- });
-}
diff --git a/test-apps/remix-website/app/lib/gh-docs/tags.test.ts b/test-apps/remix-website/app/lib/gh-docs/tags.test.ts
deleted file mode 100644
index df1943f..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/tags.test.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { getLatestVersionHeads } from "./tags";
-
-describe("getLatestVersionHeads", () => {
- it("gets the latest version heads", () => {
- let tags = [
- "1.0.0",
- "1.1.0",
- "1.1.1",
- "1.1.2",
- "2.0.0",
- "2.0.1",
- "2.1.0",
- "2.1.1",
- "3.0.0",
- "3.1.0",
- "3.2.0",
- ];
- let heads = getLatestVersionHeads(tags);
- expect(heads).toEqual(["3.2.0", "2.1.1", "1.1.2"]);
- });
-
- it("doesn't depend on order", () => {
- let tags = [
- "3.2.0",
- "1.0.0",
- "1.1.0",
- "2.1.0",
- "1.1.2",
- "1.1.1",
- "2.0.0",
- "2.0.1",
- "2.1.1",
- "3.0.0",
- "3.1.0",
- ];
- let heads = getLatestVersionHeads(tags);
- expect(heads).toEqual(["3.2.0", "2.1.1", "1.1.2"]);
- });
-});
diff --git a/test-apps/remix-website/app/lib/gh-docs/tags.ts b/test-apps/remix-website/app/lib/gh-docs/tags.ts
deleted file mode 100644
index a7c4a86..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/tags.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { LRUCache } from "lru-cache";
-import parseLinkHeader from "parse-link-header";
-import semver from "semver";
-import type { Octokit } from "octokit";
-
-type CacheContext = { octokit: Octokit; releasePackage: string };
-declare global {
- var tagsCache: LRUCache;
-}
-
-/**
- * Fetches the repo tags
- */
-export async function getTags(
- repo: string,
- { octokit, releasePackage }: CacheContext,
-) {
- return tagsCache.fetch(repo, {
- context: { octokit, releasePackage },
- });
-}
-
-export function getLatestVersion(tags: string[]) {
- return tags.filter((tag) =>
- semver.satisfies(tag, "*", { includePrerelease: false }),
- )[0];
-}
-
-/**
- * Returns the latest version of each major version
- */
-export function getLatestVersionHeads(tags: string[]) {
- let heads = new Map ();
- for (let tag of tags) {
- let major = semver.major(tag);
- let head = heads.get(major);
- if (!head || semver.gt(tag, head)) {
- heads.set(major, tag);
- }
- }
- return Array.from(heads.values()).sort(semver.compare).reverse();
-}
-
-// global for SS "HMR", we need a better story here
-global.tagsCache ??= new LRUCache({
- // let tagsCache = new LRUCache({
- max: 3,
- ttl: 1000 * 60 * 5, // 5 minutes, so we can see new tags quickly
- allowStale: true,
- noDeleteOnFetchRejection: true,
- fetchMethod: async (key, _, { context }) => {
- console.log("Fetching fresh tags (releases)");
- let [owner, repo] = key.split("/");
- return getAllReleases(owner, repo, context.releasePackage, context);
- },
-});
-
-// TODO: implementation details of the remix site leaked into here cause I'm in
-// a hurry now, sorry!
-export async function getAllReleases(
- owner: string,
- repo: string,
- primaryPackage: string,
- {
- octokit,
- page = 1,
- releases = [],
- }: {
- octokit: Octokit;
- page?: number;
- releases?: string[];
- },
-): Promise {
- console.log("Fetching fresh releases, page", page);
- let { data, headers, status } = await octokit.rest.repos.listReleases({
- mediaType: { format: "json" },
- owner,
- repo,
- per_page: 100,
- page,
- });
-
- if (status !== 200) {
- throw new Error(`Failed to fetch releases: ${data}`);
- }
-
- releases.push(
- ...data
- .filter((release) => {
- return Boolean(
- // Check the release name
- /^v[0-9]/.test(release?.name || "") ||
- // ideally all we care about is release.name, but we have some old
- // releases that don't have that set, so we check the tag name too
- // After changesets, we look for remix@
- release.tag_name.split("@")[0] === primaryPackage ||
- // pre-changesets, tag_name started with "v"
- /^v[0-9]/.test(release.tag_name),
- );
- })
- .map((release) => {
- return (
- // pre-changesets, tag_name started with "v"
- /^v[0-9]/.test(release.tag_name)
- ? release.tag_name
- : // with changesets its like remix@
- release.tag_name.split("@")[1] || "unknown"
- );
- }),
- );
-
- let parsed = parseLinkHeader(headers.link);
- if (parsed?.next) {
- return await getAllReleases(owner, repo, primaryPackage, {
- page: page + 1,
- releases,
- octokit,
- });
- }
-
- return releases;
-}
diff --git a/test-apps/remix-website/app/lib/gh-docs/tarball.server.ts b/test-apps/remix-website/app/lib/gh-docs/tarball.server.ts
deleted file mode 100644
index 0ac7b3c..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/tarball.server.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import gunzip from "gunzip-maybe";
-import tar from "tar-stream";
-
-type ProcessFile = ({
- filename,
- content,
-}: {
- filename: string;
- content: string;
-}) => Promise;
-
-export function createTarFileProcessor(
- stream: NodeJS.ReadableStream,
- pattern: RegExp = /docs\/(.+)\.md$/,
-) {
- return (processFile: ProcessFile) =>
- processFilesFromRepoTarball(stream, pattern, processFile);
-}
-
-async function processFilesFromRepoTarball(
- stream: NodeJS.ReadableStream,
- pattern: RegExp = /docs\/(.+)\.md$/,
- processFile: ProcessFile,
-): Promise {
- return new Promise((accept, reject) => {
- stream
- .pipe(gunzip())
- .pipe(tar.extract())
- .on("entry", async (header, stream, next) => {
- // Make sure the file matches the ones we want to process
- let isMatch = header.type === "file" && pattern.test(header.name);
- if (isMatch) {
- // remove "react-router-main" and "remix-v1.0.0" from the full name
- // that's something like "remix-main/docs/index.md"
- let filename = removeRepoRefName(header.name);
- // buffer the contents of this file stream so we can send the entire
- // string to be processed by the caller
- let content = await bufferStream(stream);
- await processFile({ filename, content });
- next();
- } else {
- // ignore this entry
- stream.resume();
- stream.on("end", next);
- }
- })
- .on("error", reject)
- .on("finish", accept);
- });
-}
-
-function removeRepoRefName(headerName: string): string {
- return headerName.replace(/^.+?[/]/, "");
-}
-
-async function bufferStream(stream: NodeJS.ReadableStream): Promise {
- return new Promise((accept, reject) => {
- let chunks: Uint8Array[] = [];
- stream
- .on("error", reject)
- .on("data", (chunk) => chunks.push(chunk))
- .on("end", () => accept(Buffer.concat(chunks).toString()));
- });
-}
diff --git a/test-apps/remix-website/app/lib/gh-docs/tarball.test.ts b/test-apps/remix-website/app/lib/gh-docs/tarball.test.ts
deleted file mode 100644
index 0fe492c..0000000
--- a/test-apps/remix-website/app/lib/gh-docs/tarball.test.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import fs from "fs";
-import path from "path";
-import { createTarFileProcessor } from "./tarball.server";
-
-describe("createTarFileProcessor", () => {
- it("extracts and processes files one-by-one", async () => {
- let fixturePath = path.join(process.cwd(), "test/fixture.tar.gz");
- let stream = fs.createReadStream(fixturePath);
-
- let processFiles = createTarFileProcessor(stream);
- let docs: string[] = [];
- await processFiles(async ({ filename }) => {
- docs.push(filename);
- });
-
- expect(docs).toMatchInlineSnapshot(`
- [
- "docs/api.md",
- "docs/contributing.md",
- "docs/faq.md",
- "docs/getting-started/concepts.md",
- "docs/getting-started/index.md",
- "docs/getting-started/installation.md",
- "docs/getting-started/overview.md",
- "docs/getting-started/tutorial.md",
- "docs/guides/index.md",
- "docs/guides/ssr.md",
- "docs/guides/testing.md",
- "docs/index.md",
- "docs/upgrading/index.md",
- "docs/upgrading/reach.md",
- "docs/upgrading/v5.md",
- ]
- `);
- });
-});
diff --git a/test-apps/remix-website/app/lib/github.server.ts b/test-apps/remix-website/app/lib/github.server.ts
deleted file mode 100644
index 71955e7..0000000
--- a/test-apps/remix-website/app/lib/github.server.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { Octokit } from "octokit";
-import { env } from "~/env.server";
-
-export const octokit = new Octokit(
- env.GITHUB_TOKEN ? { auth: env.GITHUB_TOKEN } : undefined,
-);
diff --git a/test-apps/remix-website/app/lib/http.server.ts b/test-apps/remix-website/app/lib/http.server.ts
deleted file mode 100644
index 8d1ca26..0000000
--- a/test-apps/remix-website/app/lib/http.server.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { redirect } from "@remix-run/node";
-import redirectsFileContents from "../../_redirects?raw";
-
-export const CACHE_CONTROL = {
- /**
- * Add 5 minutes cache control to documents and json requests to speed up the
- * back button
- */
- DEFAULT: "max-age=300",
- /**
- * Keep it in the browser (and CDN) for 5 minutes so when they click
- * back/forward/etc. it's super fast. SWR for 1 week on CDN so it stays fast,
- * but people get typos/fixes and stuff too.
- */
- doc: "max-age=300, stale-while-revalidate=604800",
- /**
- * Keep it in the browser (and CDN) for 1 day, we won't be updating these as
- * often until the conf is a bit closer, and we can prevent over-fetching from
- * Sessionize which is pretty slow.
- */
- conf: `max-age=${60 * 60 * 24}`,
-};
-
-export function requirePost(request: Request) {
- if (request.method.toLowerCase() !== "post") {
- throw new Response("", {
- status: 405,
- statusText: "Method Not Allowed",
- });
- }
-}
-
-// relative to where we're built, build/index.js
-type Redirect = [string, string, number?];
-let redirects: null | Redirect[] = null;
-
-// TODO: add support with pathToRegexp to redirect with * and stuff
-export function handleRedirects(pathname: string) {
- if (redirects === null) {
- redirects = [];
-
- for (let line of redirectsFileContents.split("\n")) {
- line = line.trim();
- if (!line || line.startsWith("#")) {
- continue;
- }
- let [from, to, maybeCode] = line.split(/\s+/);
- redirects.push([from, to, getValidRedirectCode(maybeCode)]);
- }
- }
-
- let foundRedirect = redirects.find(([from]) => pathname === from);
- if (foundRedirect) {
- throw redirect(foundRedirect[1], { status: foundRedirect[2] });
- }
-
- return null;
-}
-
-export function safeRedirect(
- to: FormDataEntryValue | string | null | undefined,
- init?: number | ResponseInit,
-) {
- if (
- !to ||
- typeof to !== "string" ||
- !to.startsWith("/") ||
- to.startsWith("//")
- ) {
- to = "/";
- }
- return redirect(to, init);
-}
-
-export function removeTrailingSlashes(request: Request) {
- let url = new URL(request.url);
- if (url.pathname.endsWith("/") && url.pathname !== "/") {
- throw redirect(url.pathname.slice(0, -1) + url.search);
- }
-}
-
-export function isProductionHost(request: Request) {
- return "remix.run" === request.headers.get("host");
-}
-
-function getValidRedirectCode(code: string | number | undefined) {
- let defaultCode = 302;
- if (!code) return defaultCode;
- if (typeof code === "string") {
- try {
- code = parseInt(code.trim(), 10);
- } catch (_) {
- return defaultCode;
- }
- }
- if (!Number.isInteger(code) || code < 300 || code >= 400) {
- return defaultCode;
- }
- return code;
-}
diff --git a/test-apps/remix-website/app/lib/md.server.ts b/test-apps/remix-website/app/lib/md.server.ts
deleted file mode 100644
index 6b14ff2..0000000
--- a/test-apps/remix-website/app/lib/md.server.ts
+++ /dev/null
@@ -1,434 +0,0 @@
-/*!
- * Forked from https://github.com/ryanflorence/md/blob/master/index.ts
- *
- * Adapted from
- * - ggoodman/nostalgie
- * - MIT https://github.com/ggoodman/nostalgie/blob/45f3f6356684287a214dab667064ec9776def933/LICENSE
- * - https://github.com/ggoodman/nostalgie/blob/45f3f6356684287a214dab667064ec9776def933/src/worker/mdxCompiler.ts
- */
-import { getHighlighter, toShikiTheme } from "shiki";
-import rangeParser from "parse-numeric-range";
-import parseFrontMatter from "front-matter";
-import type * as Hast from "hast";
-import type * as Unist from "unist";
-import type * as Shiki from "shiki";
-import type * as Unified from "unified";
-import themeJson from "../../data/base16.json";
-
-export interface ProcessorOptions {
- resolveHref?(href: string): string;
-}
-
-let processor: Awaited>;
-export async function processMarkdown(
- content: string,
- options?: ProcessorOptions,
-) {
- processor = processor || (await getProcessor(options));
- let { attributes, body: raw } = parseFrontMatter(content);
- let vfile = await processor.process(raw);
- let html = vfile.value.toString();
- return { attributes, raw, html };
-}
-
-export async function getProcessor(options?: ProcessorOptions) {
- let [
- { unified },
- { default: remarkGfm },
- { default: remarkParse },
- { default: remarkRehype },
- { default: rehypeSlug },
- { default: rehypeStringify },
- { default: rehypeAutolinkHeadings },
- plugins,
- ] = await Promise.all([
- import("unified"),
- import("remark-gfm"),
- import("remark-parse"),
- import("remark-rehype"),
- import("rehype-slug"),
- import("rehype-stringify"),
- import("rehype-autolink-headings"),
- loadPlugins(),
- ]);
-
- return unified()
- .use(remarkParse)
- .use(plugins.stripLinkExtPlugin, options)
- .use(plugins.remarkCodeBlocksShiki, options)
- .use(remarkGfm)
- .use(remarkRehype, { allowDangerousHtml: true })
- .use(rehypeStringify, { allowDangerousHtml: true })
- .use(rehypeSlug)
- .use(rehypeAutolinkHeadings);
-}
-
-type InternalPlugin<
- Input extends string | Unist.Node | undefined,
- Output,
-> = Unified.Plugin<[ProcessorOptions?], Input, Output>;
-
-export async function loadPlugins() {
- let [{ visit, SKIP }, { htmlEscape }] = await Promise.all([
- import("unist-util-visit"),
- import("escape-goat"),
- ]);
-
- const stripLinkExtPlugin: InternalPlugin = (
- options = {},
- ) => {
- return async function transformer(tree: UnistNode.Root) {
- visit(tree, "link", (node, index, parent) => {
- if (
- options.resolveHref &&
- typeof node.url === "string" &&
- isRelativeUrl(node.url)
- ) {
- if (parent && index != null) {
- parent.children[index] = {
- ...node,
- url: options.resolveHref(node.url),
- };
- return SKIP;
- }
- }
- });
- };
- };
-
- const remarkCodeBlocksShiki: InternalPlugin<
- UnistNode.Root,
- UnistNode.Root
- > = (options) => {
- let theme: ReturnType;
- let highlighterPromise: ReturnType;
-
- return async function transformer(tree: UnistNode.Root) {
- theme = theme || toShikiTheme(themeJson as any);
- highlighterPromise =
- highlighterPromise || getHighlighter({ themes: [theme] });
- let highlighter = await highlighterPromise;
- let fgColor = convertFakeHexToCustomProp(
- highlighter.getForegroundColor(theme.name) || "",
- );
- let langs: Shiki.Lang[] = [
- "js",
- "json",
- "jsx",
- "ts",
- "tsx",
- "markdown",
- "shellscript",
- "html",
- "css",
- "diff",
- "mdx",
- "prisma",
- ];
- let langSet = new Set(langs);
- let transformTasks: Array<() => Promise> = [];
-
- visit(tree, "code", (node) => {
- if (
- !node.lang ||
- !node.value ||
- !langSet.has(node.lang as Shiki.Lang)
- ) {
- return;
- }
-
- if (node.lang === "js") node.lang = "javascript";
- if (node.lang === "ts") node.lang = "typescript";
- let language = node.lang;
- let code = node.value;
- let {
- addedLines,
- highlightLines,
- nodeProperties,
- removedLines,
- startingLineNumber,
- usesLineNumbers,
- } = getCodeBlockMeta();
-
- transformTasks.push(highlightNodes);
- return SKIP;
-
- async function highlightNodes() {
- let tokens = getThemedTokens({ code, language });
- let children = tokens.map(
- (lineTokens, zeroBasedLineNumber): Hast.Element => {
- let children = lineTokens.map(
- (token): Hast.Text | Hast.Element => {
- let color = convertFakeHexToCustomProp(token.color || "");
- let content: Hast.Text = {
- type: "text",
- // Do not escape the _actual_ content
- value: token.content,
- };
-
- return color && color !== fgColor
- ? {
- type: "element",
- tagName: "span",
- properties: {
- style: `color: ${htmlEscape(color)}`,
- },
- children: [content],
- }
- : content;
- },
- );
-
- children.push({
- type: "text",
- value: "\n",
- });
-
- let isDiff = addedLines.length > 0 || removedLines.length > 0;
- let diffLineNumber = startingLineNumber - 1;
- let lineNumber = zeroBasedLineNumber + startingLineNumber;
- let highlightLine = highlightLines?.includes(lineNumber);
- let removeLine = removedLines.includes(lineNumber);
- let addLine = addedLines.includes(lineNumber);
- if (!removeLine) {
- diffLineNumber++;
- }
-
- return {
- type: "element",
- tagName: "span",
- properties: {
- className: "codeblock-line",
- dataHighlight: highlightLine ? "true" : undefined,
- dataLineNumber: usesLineNumbers ? lineNumber : undefined,
- dataAdd: isDiff ? addLine : undefined,
- dataRemove: isDiff ? removeLine : undefined,
- dataDiffLineNumber: isDiff ? diffLineNumber : undefined,
- },
- children,
- };
- },
- );
-
- let nodeValue = {
- type: "element",
- tagName: "pre",
- properties: {
- ...nodeProperties,
- dataLineNumbers: usesLineNumbers ? "true" : "false",
- dataLang: htmlEscape(language),
- style: `color: ${htmlEscape(fgColor)};`,
- },
- children: [
- {
- type: "element",
- tagName: "code",
- children,
- },
- ],
- };
-
- let data = node.data ?? {};
- (node as any).type = "element";
- (node as any).tagName = "div";
- let properties =
- data.hProperties && typeof data.hProperties === "object"
- ? data.hProperties
- : {};
- data.hProperties = {
- ...properties,
- dataCodeBlock: "",
- ...nodeProperties,
- dataLineNumbers: usesLineNumbers ? "true" : "false",
- dataLang: htmlEscape(language),
- };
- data.hChildren = [nodeValue];
- node.data = data;
- }
-
- function getCodeBlockMeta() {
- // TODO: figure out how this is ever an array?
- let meta = Array.isArray(node.meta) ? node.meta[0] : node.meta;
-
- let metaParams = new URLSearchParams();
- if (meta) {
- let linesHighlightsMetaShorthand = meta.match(/^\[(.+)\]$/);
- if (linesHighlightsMetaShorthand) {
- metaParams.set("lines", linesHighlightsMetaShorthand[0]);
- } else {
- metaParams = new URLSearchParams(meta.split(/\s+/).join("&"));
- }
- }
-
- let addedLines = parseLineHighlights(metaParams.get("add"));
- let removedLines = parseLineHighlights(metaParams.get("remove"));
- let highlightLines = parseLineHighlights(metaParams.get("lines"));
- let startValNum = metaParams.has("start")
- ? Number(metaParams.get("start"))
- : 1;
- let startingLineNumber = Number.isFinite(startValNum)
- ? startValNum
- : 1;
- let usesLineNumbers = !metaParams.has("nonumber");
-
- let nodeProperties: { [key: string]: string } = {};
- metaParams.forEach((val, key) => {
- if (key === "lines") return;
- nodeProperties[`data-${key}`] = val;
- });
-
- return {
- addedLines,
- highlightLines,
- nodeProperties,
- removedLines,
- startingLineNumber,
- usesLineNumbers,
- };
- }
- });
-
- await Promise.all(transformTasks.map((exec) => exec()));
-
- function getThemedTokens({
- code,
- language,
- }: {
- code: string;
- language: Shiki.Lang;
- }) {
- return highlighter.codeToThemedTokens(code, language, theme.name, {
- includeExplanation: false,
- });
- }
- };
- };
-
- return {
- stripLinkExtPlugin,
- remarkCodeBlocksShiki,
- };
-}
-
-////////////////////////////////////////////////////////////////////////////////
-
-function parseLineHighlights(param: string | null) {
- if (!param) return [];
- let range = param.match(/^\[(.+)\]$/);
- if (!range) return [];
- return rangeParser(range[1]);
-}
-
-// The theme actually stores #FFFF${base-16-color-id} because vscode-textmate
-// requires colors to be valid hex codes, if they aren't, it changes them to a
-// default, so this is a mega hack to trick it.
-function convertFakeHexToCustomProp(color: string) {
- return color.replace(/^#FFFF(.+)/, "var(--base$1)");
-}
-
-function isRelativeUrl(test: string) {
- // Probably fragile but should work well enough.
- // It would be nice if the consumer could provide a baseURI we could do
- // something like:
- // new URL(baseURI).origin === new URL(test, baseURI).origin
- let regexp = new RegExp("^(?:[a-z]+:)?//", "i");
- return !regexp.test(test);
-}
-
-////////////////////////////////////////////////////////////////////////////////
-
-export namespace UnistNode {
- export type Content = Flow | Phrasing | Html;
- export interface Root extends Unist.Parent {
- type: "root";
- children: Flow[];
- }
-
- export type Flow =
- | Blockquote
- | Heading
- | ParagraphNode
- | Link
- | Pre
- | Code
- | Image
- | Element
- | Html;
-
- export interface Html extends Unist.Node {
- type: "html";
- value: string;
- }
-
- export interface Element extends Unist.Parent {
- type: "element";
- tagName?: string;
- }
-
- export interface CodeElement extends Element {
- tagName: "code";
- data?: {
- meta?: string;
- };
- properties?: {
- className?: string[];
- };
- }
-
- export interface PreElement extends Element {
- tagName: "pre";
- }
-
- export interface Image extends Unist.Node {
- type: "image";
- title: null;
- url: string;
- alt?: string;
- }
-
- export interface Blockquote extends Unist.Parent {
- type: "blockquote";
- children: Flow[];
- }
-
- export interface Heading extends Unist.Parent {
- type: "heading";
- depth: number;
- children: UnistNode.Phrasing[];
- }
-
- interface ParagraphNode extends Unist.Parent {
- type: "paragraph";
- children: Phrasing[];
- }
-
- export interface Pre extends Unist.Parent {
- type: "pre";
- children: Phrasing[];
- }
-
- export interface Code extends Unist.Parent {
- type: "code";
- value?: string;
- lang?: Shiki.Lang;
- meta?: string | string[];
- }
-
- export type Phrasing = Text | Emphasis;
-
- export interface Emphasis extends Unist.Parent {
- type: "emphasis";
- children: Phrasing[];
- }
-
- export interface Link extends Unist.Parent {
- type: "link";
- children: Flow[];
- url?: string;
- }
-
- export interface Text extends Unist.Literal {
- type: "text";
- value: string;
- }
-}
diff --git a/test-apps/remix-website/app/lib/mdtut.server.ts b/test-apps/remix-website/app/lib/mdtut.server.ts
deleted file mode 100644
index ba06bea..0000000
--- a/test-apps/remix-website/app/lib/mdtut.server.ts
+++ /dev/null
@@ -1,238 +0,0 @@
-import { loadPlugins, type UnistNode } from "~/lib/md.server";
-import { LRUCache } from "lru-cache";
-import invariant from "tiny-invariant";
-import type * as Hast from "hast";
-import type * as Mdast from "mdast";
-import type * as Unist from "unist";
-
-const mdContentsByFilename = Object.fromEntries(
- Object.entries(
- import.meta.glob("../../md/**/*.md", {
- query: "?raw",
- import: "default",
- eager: true,
- }),
- ).map(([filePath, contents]) => {
- invariant(
- typeof contents === "string",
- `Expected ${filePath} to be a string, but got ${typeof contents}`,
- );
- return [filePath.replace("../../md/", ""), contents];
- }),
-);
-
-const STATE_NORMAL = "NORMAL";
-const STATE_SEQUENCING = "SEQUENCING";
-type State = typeof STATE_NORMAL | typeof STATE_SEQUENCING;
-
-const cache = new LRUCache({
- maxSize: 1024 * 1024 * 12, // 12 mb
- sizeCalculation(value, key) {
- return JSON.stringify(value).length + (key ? key.length : 0);
- },
-});
-
-type TProcessorPromise = ReturnType;
-let processorPromise: TProcessorPromise;
-
-async function getProcessor() {
- let [
- { unified },
- { default: remarkParse },
- { default: remarkHtml },
- plugins,
- ] = await Promise.all([
- import("unified"),
- import("remark-parse"),
- import("remark-html"),
- loadPlugins(),
- ]);
-
- return unified()
- .use(remarkParse)
- .use(plugins.remarkCodeBlocksShiki)
- .use(remarkHtml, { sanitize: false });
-}
-
-async function getMarkdownTutPage(filename: string): Promise {
- processorPromise = processorPromise || getProcessor();
- let processor = await processorPromise;
- let cached = cache.get(filename);
- if (cached) {
- return cached;
- }
- let file = mdContentsByFilename[filename];
- if (!file) {
- throw new Error('File not found: "' + filename + '"');
- }
- let page = (await processMarkdown(processor, file)) as MarkdownTutPage;
- cache.set(filename, page);
- return page;
-}
-
-////////////////////////////////////////////////////////////////////////////////
-
-type TProcessor = Awaited;
-
-async function processMarkdown(
- processor: TProcessor,
- content: string | Buffer,
-): Promise {
- let state: State = STATE_NORMAL;
- let tree = await processor.run(processor.parse(content) as UnistNode.Root);
- let current: ProseNode | SlideNode = {
- type: "prose",
- children: [],
- };
- let root: Array = [current];
- for (let node of tree.children) {
- if (state === STATE_NORMAL) {
- if (isSequenceFence(node)) {
- startSequencing();
- } else {
- addNode(node);
- }
- } else if (state === STATE_SEQUENCING) {
- if (isSequenceFence(node)) {
- stopSequencing();
- } else {
- if (isSequenceBreak(node)) {
- addSlide();
- } else {
- addNode(node);
- }
- }
- }
- }
-
- return stringify(processor, root) as MarkdownTutPage;
-
- function addNode(node: UnistNode.Flow) {
- current.children.push(node);
- }
-
- function addSlide() {
- let sequence = root[root.length - 1];
- if (sequence.type !== "sequence") {
- throw new Error(`Expected sequence, got "${sequence.type}"`);
- }
- let slide: SlideNode = {
- type: "slide",
- children: [],
- };
- sequence.children.push(slide);
- current = slide;
- }
-
- function startSequencing() {
- state = STATE_SEQUENCING;
- let sequence: SequenceNode = {
- type: "sequence",
- children: [],
- };
- root.push(sequence);
- let slide: SlideNode = {
- type: "slide",
- children: [],
- };
- sequence.children.push(slide);
- current = slide;
- }
-
- function stopSequencing() {
- state = STATE_NORMAL;
- current = {
- type: "prose",
- children: [],
- };
- root.push(current);
- }
-}
-
-function stringify(
- processor: TProcessor,
- root: Array,
-): MarkdownTutPage {
- let page: MarkdownTutPage = [];
- for (let node of root) {
- if (node.type === "prose") {
- page.push({
- type: "prose",
- html: processor
- .stringify({ type: "root", children: node.children } as Mdast.Root)
- .trim(),
- });
- } else if (node.type === "sequence") {
- let slides: Slide[] = [];
- for (let slideNode of node.children) {
- let length = slideNode.children.length;
- let subjectNode = slideNode.children[length - 1];
- let childNodes = slideNode.children.slice(0, length - 1);
- let html = processor
- .stringify({ type: "root", children: childNodes } as Mdast.Root)
- .trim();
- let subject = processor.stringify(subjectNode as Mdast.Root).trim();
- slides.push({ type: "slide", subject, html });
- }
- page.push({ type: "sequence", slides });
- }
- }
- return page;
-}
-
-function isSequenceFence(
- node: N,
-): node is N & { type: "paragraph"; children: [UnistNode.Text] } {
- if (node.type !== "paragraph" || !Array.isArray(node.children)) {
- return false;
- }
- let firstChild: unknown = node.children[0];
- return !!(
- firstChild &&
- typeof firstChild === "object" &&
- "value" in firstChild &&
- firstChild.value === ":::"
- );
-}
-
-function isSequenceBreak(
- node: N,
-): node is N & { type: "thematicBreak" } {
- return "type" in node && node.type === "thematicBreak";
-}
-
-type MarkdownTutPage = (Prose | Sequence)[];
-
-interface Prose {
- type: "prose";
- html: string;
-}
-
-interface Sequence {
- type: "sequence";
- slides: Slide[];
-}
-
-interface Slide {
- type: "slide";
- html: string;
- subject: string;
-}
-
-interface ProseNode extends Unist.Parent {
- type: "prose";
- children: UnistNode.Flow[];
-}
-
-interface SequenceNode extends Unist.Parent {
- type: "sequence";
- children: SlideNode[];
-}
-
-interface SlideNode extends Unist.Parent {
- type: "slide";
- children: Hast.Node[];
-}
-
-export { getMarkdownTutPage, getProcessor };
-export type { MarkdownTutPage, Prose, Sequence, Slide };
diff --git a/test-apps/remix-website/app/lib/meta/index.ts b/test-apps/remix-website/app/lib/meta/index.ts
deleted file mode 100644
index 799ac47..0000000
--- a/test-apps/remix-website/app/lib/meta/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { getMeta } from "./meta";
diff --git a/test-apps/remix-website/app/lib/meta/meta.test.ts b/test-apps/remix-website/app/lib/meta/meta.test.ts
deleted file mode 100644
index 684b801..0000000
--- a/test-apps/remix-website/app/lib/meta/meta.test.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { getMeta } from "./meta";
-
-describe("getMeta", () => {
- describe("default", () => {
- it("return basic site info without siteUrl&image", () => {
- expect(
- getMeta({
- title: "Remix",
- description: "Some description.",
- }),
- ).toStrictEqual([
- { title: "Remix" },
- { name: "description", content: "Some description." },
- { property: "og:title", content: "Remix" },
- { property: "og:description", content: "Some description." },
- { name: "twitter:card", content: "summary_large_image" },
- { name: "twitter:creator", content: "@remix_run" },
- { name: "twitter:site", content: "@remix_run" },
- { name: "twitter:title", content: "Remix" },
- { name: "twitter:description", content: "Some description." },
- ]);
- });
- });
-
- describe("with siteUrl and image", () => {
- it("return basic site info", () => {
- expect(
- getMeta({
- title: "Remix",
- description: "Some description.",
- siteUrl: "https://remix.run",
- image: "https://remix.run/img/og.1.jpg",
- }),
- ).toStrictEqual([
- { title: "Remix" },
- { name: "description", content: "Some description." },
- { property: "og:url", content: "https://remix.run" },
- { property: "og:title", content: "Remix" },
- { property: "og:description", content: "Some description." },
- { property: "og:image", content: "https://remix.run/img/og.1.jpg" },
- { name: "twitter:card", content: "summary_large_image" },
- { name: "twitter:creator", content: "@remix_run" },
- { name: "twitter:site", content: "@remix_run" },
- { name: "twitter:title", content: "Remix" },
- { name: "twitter:description", content: "Some description." },
- { name: "twitter:image", content: "https://remix.run/img/og.1.jpg" },
- ]);
- });
- });
-
- describe("with additionalMeta", () => {
- it("plus basic info, return additional meta data", () => {
- expect(
- getMeta({
- title: "Remix",
- description: "Some description.",
- additionalMeta: [
- { name: "robots", content: "index, follow" },
- { name: "author", content: "Remix" },
- { name: "keyword", content: undefined },
- ],
- }),
- ).toStrictEqual([
- { title: "Remix" },
- { name: "description", content: "Some description." },
- { property: "og:title", content: "Remix" },
- { property: "og:description", content: "Some description." },
- { name: "twitter:card", content: "summary_large_image" },
- { name: "twitter:creator", content: "@remix_run" },
- { name: "twitter:site", content: "@remix_run" },
- { name: "twitter:title", content: "Remix" },
- { name: "twitter:description", content: "Some description." },
- { name: "robots", content: "index, follow" },
- { name: "author", content: "Remix" },
- ]);
- });
- });
-});
diff --git a/test-apps/remix-website/app/lib/meta/meta.ts b/test-apps/remix-website/app/lib/meta/meta.ts
deleted file mode 100644
index 56ac492..0000000
--- a/test-apps/remix-website/app/lib/meta/meta.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { MetaDescriptor } from "@remix-run/node";
-
-type CustomMetaArgs = {
- title: string;
- description: string;
- siteUrl?: string;
- image?: string;
-} & { additionalMeta?: MetaDescriptor[] };
-
-export const getMeta = ({
- title,
- description,
- siteUrl,
- image,
- additionalMeta,
-}: CustomMetaArgs) => {
- return [
- { title },
- { name: "description", content: description },
- { property: "og:url", content: siteUrl },
- { property: "og:title", content: title },
- { property: "og:description", content: description },
- { property: "og:image", content: image },
- { name: "twitter:card", content: "summary_large_image" },
- { name: "twitter:creator", content: "@remix_run" },
- { name: "twitter:site", content: "@remix_run" },
- { name: "twitter:title", content: title },
- { name: "twitter:description", content: description },
- { name: "twitter:image", content: image },
- ...(additionalMeta ?? []),
- ].filter((v) => {
- if ("content" in v) {
- return !!v.content;
- }
- return true;
- });
-};
diff --git a/test-apps/remix-website/app/lib/observe-rect.ts b/test-apps/remix-website/app/lib/observe-rect.ts
deleted file mode 100644
index ebcce58..0000000
--- a/test-apps/remix-website/app/lib/observe-rect.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-const RECT_PROPS = [
- "bottom",
- "height",
- "left",
- "right",
- "top",
- "width",
-] as const;
-
-let observedNodes = new Map();
-let rafId: number;
-
-function getRect(node: Element): DOMRect {
- return node.getBoundingClientRect();
-}
-
-function run() {
- let changedStates: RectProps[] = [];
- observedNodes.forEach((state, node) => {
- let newRect = getRect(node);
- if (rectChanged(newRect, state.rect)) {
- state.rect = newRect;
- changedStates.push(state);
- }
- });
-
- changedStates.forEach((state) => {
- state.callbacks.forEach((cb) => cb(state.rect));
- });
-
- rafId = window.requestAnimationFrame(run);
-}
-
-function observeRect(node: Element, onChange: (rect: DOMRect) => void) {
- return {
- observe() {
- let wasEmpty = observedNodes.size === 0;
- if (observedNodes.has(node)) {
- observedNodes.get(node)!.callbacks.push(onChange);
- } else {
- observedNodes.set(node, {
- rect: undefined,
- hasRectChanged: false,
- callbacks: [onChange],
- });
- }
- if (wasEmpty) run();
- },
-
- unobserve() {
- let state = observedNodes.get(node);
- if (state) {
- // Remove the callback
- let index = state.callbacks.indexOf(onChange);
- if (index >= 0) state.callbacks.splice(index, 1);
-
- // Remove the node reference
- if (!state.callbacks.length) observedNodes.delete(node);
-
- // Stop the loop
- if (!observedNodes.size) cancelAnimationFrame(rafId);
- }
- },
- };
-}
-
-function rectChanged(a: DOMRect, b: DOMRect | undefined) {
- return RECT_PROPS.some((prop) => a[prop] !== b?.[prop]);
-}
-
-export { getRect, observeRect, rectChanged };
-
-export interface RectProps {
- rect: DOMRect | undefined;
- hasRectChanged: boolean;
- callbacks: Function[];
-}
diff --git a/test-apps/remix-website/app/lib/resources.server/index.ts b/test-apps/remix-website/app/lib/resources.server/index.ts
deleted file mode 100644
index c0146cb..0000000
--- a/test-apps/remix-website/app/lib/resources.server/index.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-import yaml from "yaml";
-import { LRUCache } from "lru-cache";
-import { env } from "~/env.server";
-import { processMarkdown } from "../md.server";
-import resourcesYamlFileContents from "../../../data/resources.yaml?raw";
-import { slugify } from "~/ui/primitives/utils";
-import type { Octokit } from "octokit";
-import type { ResourceYamlData } from "~/schemas/yaml-resource-schema";
-import { yamlResourceSchema } from "~/schemas/yaml-resource-schema";
-
-export type CacheContext = { octokit: Octokit };
-
-const GITHUB_URL = "https://github.com";
-
-export const fetchResourcesFromYaml = () => {
- return yamlResourceSchema.parse(yaml.parse(resourcesYamlFileContents));
-};
-
-let _resources = fetchResourcesFromYaml();
-
-let starFormatter = new Intl.NumberFormat("en", { notation: "compact" });
-
-type ResourceGitHubData = {
- description?: string;
- sponsorUrl?: string;
- stars: number;
- starsFormatted: string;
- tags: string[];
-};
-
-export type Resource = ResourceYamlData & ResourceGitHubData;
-
-export type Category = "all" | ResourceYamlData["category"];
-
-/**
- * Gets all of the resources, fetching and merging GitHub data for each one
- */
-export async function getAllResources({ octokit }: CacheContext) {
- let resources: Resource[] = await Promise.all(
- _resources.map(async (resource) => {
- // This is cached, so should just be a simple lookup
- let gitHubData = await getResourceGitHubData(resource.repoUrl, {
- octokit,
- });
- if (!gitHubData) {
- throw new Error(`Could not find GitHub data for ${resource.repoUrl}`);
- }
- return { ...resource, ...gitHubData };
- }),
- );
-
- return resources.sort((a, b) => b.stars - a.stars);
-}
-
-/**
- * Replace relative links in the README with absolute links
- *
- * Works only with images
- *
- * @param inputString - The README string
- * @param repoUrl - The URL of the repository
- * @returns The README string with relative links replaced with absolute links
- *
- * @example
- * const input = ` `;
- * const repoUrl = "https://my-repo";
- * const readme = replaceRelativeLinks(input, repoUrl);
- * console.log(readme); //
- *
- */
-
-export function replaceRelativeLinks(inputString: string, repoUrl: string) {
- // Regular expression to match slugify(resource.title) === resourceSlug,
- );
-
- if (!resource) return;
-
- let [gitHubData, readmeHtml] = await Promise.all([
- getResourceGitHubData(resource.repoUrl, { octokit }),
- getResourceReadme(resource.repoUrl, { octokit }),
- ]);
-
- if (!gitHubData || !readmeHtml) {
- throw new Error(`Could not find GitHub data for ${resource.repoUrl}`);
- }
-
- return {
- ...resource,
- ...gitHubData,
- readmeHtml,
- };
-}
-
-//#region LRUCache and fetchers for GitHub data and READMEs
-
-declare global {
- var resourceReadmeCache: LRUCache;
- var resourceGitHubDataCache: LRUCache<
- string,
- ResourceGitHubData,
- CacheContext
- >;
-}
-
-let NO_CACHE = env.NO_CACHE;
-
-global.resourceReadmeCache ??= new LRUCache({
- max: 300,
- ttl: NO_CACHE ? 1 : 1000 * 60 * 5, // 5 minutes
- allowStale: !NO_CACHE,
- noDeleteOnFetchRejection: true,
- fetchMethod: fetchReadme,
-});
-
-async function fetchReadme(
- key: string,
- _staleValue: string | undefined,
- { context }: LRUCache.FetchOptionsWithContext,
-): Promise {
- let [owner, repo] = key.split("/");
- let contents = await context.octokit.rest.repos.getReadme({
- owner,
- repo,
- mediaType: { format: "raw" },
- });
-
- // when using `format: raw` the data property is the file contents
- let md = contents.data as unknown;
- if (md == null || typeof md !== "string") {
- throw Error(`Could not find README in ${key}`);
- }
- let { html } = await processMarkdown(md);
- return replaceRelativeLinks(html, `${GITHUB_URL}/${key}`);
-}
-
-async function getResourceReadme(repoUrl: string, context: CacheContext) {
- let repo = repoUrl.replace(`${GITHUB_URL}/`, "");
- let doc = await resourceReadmeCache.fetch(repo, { context });
-
- return doc || undefined;
-}
-
-async function getSponsorUrl(owner: string) {
- let sponsorUrl = `${GITHUB_URL}/sponsors/${owner}`;
-
- try {
- let response = await fetch(sponsorUrl);
- return !response.redirected ? sponsorUrl : undefined;
- } catch (e) {
- console.error("Failed to fetch sponsor url for", owner);
- return undefined;
- }
-}
-
-async function getResourceGitHubData(
- repoUrl: string,
- { octokit }: CacheContext,
-) {
- return resourceGitHubDataCache.fetch(repoUrl, {
- context: { octokit },
- });
-}
-
-global.resourceGitHubDataCache ??= new LRUCache<
- string,
- ResourceGitHubData,
- CacheContext
->({
- max: 300,
- ttl: NO_CACHE ? 1 : 1000 * 60 * 5, // 5 minutes
- allowStale: !NO_CACHE,
- noDeleteOnFetchRejection: true,
- fetchMethod: fetchResourceGitHubData,
-});
-
-let ignoredTopics = new Set(["remix-stack", "remix-run", "remix"]);
-
-async function fetchResourceGitHubData(
- repoUrl: string,
- staleValue: ResourceGitHubData | undefined,
- {
- context,
- }: LRUCache.FetchOptionsWithContext,
-): Promise {
- let [owner, repo] = repoUrl.replace(`${GITHUB_URL}/`, "").split("/");
-
- let [{ data }, sponsorUrl] = await Promise.all([
- context.octokit.rest.repos.get({ owner, repo }),
- getSponsorUrl(owner),
- ]);
-
- let description = data.description ?? undefined;
- let stars = data.stargazers_count;
- let tags = (data.topics ?? []).filter((topic) => !ignoredTopics.has(topic));
-
- return {
- description,
- stars,
- starsFormatted: starFormatter.format(stars).toLowerCase(),
- tags,
- sponsorUrl,
- };
-}
-
-//#endregion
diff --git a/test-apps/remix-website/app/lib/resources.server/resources.test.ts b/test-apps/remix-website/app/lib/resources.server/resources.test.ts
deleted file mode 100644
index a3e7160..0000000
--- a/test-apps/remix-website/app/lib/resources.server/resources.test.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { fetchResourcesFromYaml, replaceRelativeLinks } from "./index";
-
-describe("replaceRelativeLinks", () => {
- it("should replace relative links with absolute links for images", () => {
- const input = ` `;
- const repoUrl = "https://my-repo";
- const expected = ` `;
- expect(replaceRelativeLinks(input, repoUrl)).toEqual(expected);
- });
-
- it("should not replace relative links with absolute links for links", () => {
- const input = ` `;
- const repoUrl = "https://my-repo";
-
- expect(replaceRelativeLinks(input, repoUrl)).toEqual(input);
- });
-
- it("should replace relative links with absolute links when src is not the first attribute", () => {
- const input = ` `;
- const repoUrl = "https://my-repo";
- const expected = ` `;
- expect(replaceRelativeLinks(input, repoUrl)).toEqual(expected);
- });
-
- it("should replace relative links with absolute links for images with other attributes and multiple images and other tags", () => {
- const input = ` `;
- const repoUrl = "https://my-repo";
- const expected = ` `;
- expect(replaceRelativeLinks(input, repoUrl)).toEqual(expected);
- });
-
- it("should not replace relative links with absolute links for images with absolute links", () => {
- const input = ` `;
- const repoUrl = "https://my-repo";
- expect(replaceRelativeLinks(input, repoUrl)).toEqual(input);
- });
-
- it("should not replace relative links with absolute links for images with absolute links and replace with relative links properly", () => {
- const input = ` `;
- const repoUrl = "https://my-repo";
- const expected = ` `;
- expect(replaceRelativeLinks(input, repoUrl)).toEqual(expected);
- });
-
- it("should not replace relative links that are not in the src attribute", () => {
- const input = ` `;
- const repoUrl = "https://my-repo";
- expect(replaceRelativeLinks(input, repoUrl)).toEqual(input);
- });
-
- it("should not replace relative links that are not in the img tag", () => {
- const input = `you can find it here ./relative`;
- const repoUrl = "https://my-repo";
- expect(replaceRelativeLinks(input, repoUrl)).toEqual(input);
- });
-});
-
-describe("fetchResourcesFromYaml", () => {
- it("fetches resources from the yaml file properly and returns an array of resources", () => {
- const resources = fetchResourcesFromYaml();
- expect(resources.length).toBeGreaterThan(0);
- });
-});
diff --git a/test-apps/remix-website/app/lib/showcase.server.ts b/test-apps/remix-website/app/lib/showcase.server.ts
deleted file mode 100644
index b17587f..0000000
--- a/test-apps/remix-website/app/lib/showcase.server.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import yaml from "yaml";
-import showcaseYamlFileContents from "../../data/showcase.yaml?raw";
-
-export const showcaseExamples: ShowcaseExample[] = yaml.parse(
- showcaseYamlFileContents,
-);
-
-export interface ShowcaseExample {
- name: string;
- description: string;
- link: string;
- imgSrc: string;
- videoSrc: string;
-}
diff --git a/test-apps/remix-website/app/root.tsx b/test-apps/remix-website/app/root.tsx
deleted file mode 100644
index ad02b89..0000000
--- a/test-apps/remix-website/app/root.tsx
+++ /dev/null
@@ -1,232 +0,0 @@
-import * as React from "react";
-import {
- isRouteErrorResponse,
- Links,
- Meta,
- Outlet,
- Scripts,
- ScrollRestoration,
- useLoaderData,
- useMatches,
- useRouteError,
-} from "@remix-run/react";
-import { json } from "@remix-run/node";
-import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
-import {
- load as loadFathom,
- type LoadOptions as FathomLoadOptions,
-} from "fathom-client";
-import "~/styles/tailwind.css";
-import "~/styles/bailwind.css";
-import { removeTrailingSlashes, isProductionHost } from "~/lib/http.server";
-import { ColorSchemeScript, useColorScheme } from "~/lib/color-scheme";
-import { parseColorScheme } from "~/lib/color-scheme.server";
-import iconsHref from "~/icons.svg";
-import cx from "clsx";
-import { canUseDOM } from "./ui/primitives/utils";
-import { GlobalLoading } from "./ui/global-loading";
-
-export async function loader({ request }: LoaderFunctionArgs) {
- removeTrailingSlashes(request);
-
- let isDevHost = !isProductionHost(request);
- let url = new URL(request.url);
-
- let colorScheme = await parseColorScheme(request);
-
- return json(
- {
- colorScheme,
- host: url.host,
- isProductionHost: !isDevHost,
- noIndex:
- isDevHost ||
- url.pathname === "/docs/en/v1/api/remix" ||
- url.pathname === "/docs/en/v1/api/conventions",
- },
- {
- headers: {
- Vary: "Cookie",
- },
- },
- );
-}
-
-export function links() {
- let preloadedFonts = [
- "inter-roman-latin-var.woff2",
- "inter-italic-latin-var.woff2",
- "source-code-pro-roman-var.woff2",
- "source-code-pro-italic-var.woff2",
- ];
- return [
- { rel: "icon", href: "/favicon-32.png", sizes: "32x32" },
- { rel: "icon", href: "/favicon-128.png", sizes: "128x128" },
- { rel: "icon", href: "/favicon-180.png", sizes: "180x180" },
- { rel: "icon", href: "/favicon-192.png", sizes: "192x192" },
- { rel: "apple-touch-icon", href: "/favicon-180.png", sizes: "180x180" },
- ...preloadedFonts.map((font) => ({
- rel: "preload",
- as: "font",
- href: `/font/${font}`,
- crossOrigin: "anonymous",
- })),
- { rel: "alternate", type: "application/rss+xml", href: "/blog/rss.xml" },
- ];
-}
-
-
-
-interface DocumentProps {
- title?: string;
- forceDark?: boolean;
- darkBg?: string;
- isDev?: boolean;
- noIndex: boolean;
- children: React.ReactNode;
-}
-
-function Document({
- children,
- title,
- forceDark,
- darkBg,
- noIndex,
- isDev,
-}: DocumentProps) {
- let colorScheme = useColorScheme();
- let matches = useMatches();
- let isDocsPage = !!matches.find((match) =>
- match.id.startsWith("routes/docs"),
- );
-
- return (
-
-
-
-
-
-
- {noIndex && }
-
-
- {title && {title} }
-
-
-
-
- {children}
-
-
-
-
- );
-}
-
-export default function App() {
- let matches = useMatches();
- let { noIndex } = useLoaderData();
- let forceDark = matches.some(({ handle }) => {
- if (handle && typeof handle === "object" && "forceDark" in handle) {
- return handle.forceDark;
- }
- return false;
- });
-
- if (process.env.NODE_ENV !== "development") {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- useFathomClient("IRVDGCHK", {
- url: "https://cdn.usefathom.com/script.js",
- spa: "history",
- excludedDomains: ["localhost"],
- });
- }
-
- return (
-
-
-
-
- );
-}
-
-export function ErrorBoundary() {
- let error = useRouteError();
- if (!canUseDOM) {
- console.error(error);
- }
-
- if (isRouteErrorResponse(error)) {
- return (
-
-
-
- );
- }
-
- return (
-
-
-
-
Error
-
- Something went wrong! Please try again later.
-
-
-
-
- );
-}
-
-function useFathomClient(siteId: string, loadOptions: FathomLoadOptions) {
- let loaded = React.useRef(false);
- React.useEffect(() => {
- if (loaded.current) return;
- loadFathom(siteId, loadOptions);
- loaded.current = true;
- }, [loadOptions, siteId]);
-}
diff --git a/test-apps/remix-website/app/routes/$.tsx b/test-apps/remix-website/app/routes/$.tsx
deleted file mode 100644
index d81f9b2..0000000
--- a/test-apps/remix-website/app/routes/$.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { handleRedirects } from "~/lib/http.server";
-import type { LoaderFunctionArgs } from "@remix-run/node";
-import { redirect, json } from "@remix-run/node";
-import { getRepoDoc } from "~/lib/gh-docs";
-
-// We use the catch-all route to attempt to find a doc for the given path. If a
-// doc isn't found, we return a 404 as expected. However we also log those
-// errors to get a good idea of what weird paths are requested often to identify
-// a block-list for bots and malicious actors.
-//
-// We can skip all of that if a request is made for a static file, which we know
-// doesn't exist by the time we get to the catch all route as our request
-// handler will hit the public directory first. It'll skip the logging and save
-// us an unncessary DB query.
-const SAFE_STATIC_FILE_EXTENSIONS = [
- ".html",
- ".css",
- ".js",
- ".txt",
- ".json",
- ".ico",
- ".svg",
- ".png",
- ".jpg",
- ".jpeg",
- ".gif",
- ".woff",
- ".woff2",
- ".ttf",
- ".eot",
- ".otf",
- ".mp4",
- ".webm",
- ".ogg",
- ".mp3",
- ".wav",
- ".flac",
- ".aac",
- ".m4a",
- ".mov",
-];
-
-function handleStaticFileRequests(param: string | undefined) {
- if (
- SAFE_STATIC_FILE_EXTENSIONS.some((ext) => !!(param && param.endsWith(ext)))
- ) {
- throw json({}, { status: 404 });
- }
-}
-
-export const loader = async ({ request, params }: LoaderFunctionArgs) => {
- handleRedirects(new URL(request.url).pathname);
- handleStaticFileRequests(params["*"]);
-
- try {
- let ref = "main";
- let lang = "en";
- let doc = await getRepoDoc(ref, `docs/${params["*"]}`);
- if (!doc) throw null;
- // FIXME: This results in two fetches, as the loader for the docs page will
- // repeat the request cycle. This isn't a problem if the doc is in the LRU
- // cache but we should probably fix it anyway.
- return redirect(`/docs/${lang}/${ref}/${params["*"]}`);
- } catch (_) {}
- throw json({}, { status: 404 });
-};
-
-export default function () {
- return null;
-}
diff --git a/test-apps/remix-website/app/routes/[_]actions.color-scheme.tsx b/test-apps/remix-website/app/routes/[_]actions.color-scheme.tsx
deleted file mode 100644
index 147acab..0000000
--- a/test-apps/remix-website/app/routes/[_]actions.color-scheme.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import {
- serializeColorScheme,
- validateColorScheme,
-} from "~/lib/color-scheme.server";
-import { safeRedirect } from "~/lib/http.server";
-
-export async function action({ request }: ActionFunctionArgs) {
- let formData = await request.formData();
- let colorScheme = formData.get("colorScheme");
- if (!validateColorScheme(colorScheme)) {
- throw new Response("Bad Request", { status: 400 });
- }
-
- return safeRedirect(formData.get("returnTo"), {
- headers: { "Set-Cookie": await serializeColorScheme(colorScheme) },
- });
-}
diff --git a/test-apps/remix-website/app/routes/[_]actions.newsletter.tsx b/test-apps/remix-website/app/routes/[_]actions.newsletter.tsx
deleted file mode 100644
index e0a7f7b..0000000
--- a/test-apps/remix-website/app/routes/[_]actions.newsletter.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { json } from "@remix-run/node";
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { subscribeToNewsletter } from "~/lib/convertkit";
-import { requirePost } from "~/lib/http.server";
-
-export const action = async ({ request }: ActionFunctionArgs) => {
- requirePost(request);
-
- let body = new URLSearchParams(await request.text());
- let email = body.get("email");
- if (typeof email !== "string" || email.indexOf("@") === -1) {
- return json({ error: "Invalid Email", ok: false }, { status: 400 });
- }
-
- try {
- await subscribeToNewsletter(email);
- } catch (e: any) {
- return json({ error: e.message || "Unknown error", ok: false });
- }
-
- return json({ error: null, ok: true });
-};
diff --git a/test-apps/remix-website/app/routes/[_]docs.routing-index.tsx b/test-apps/remix-website/app/routes/[_]docs.routing-index.tsx
deleted file mode 100644
index 4e82a0c..0000000
--- a/test-apps/remix-website/app/routes/[_]docs.routing-index.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { BrowserChrome } from "~/ui/browser-chrome";
-import * as Fakebooks from "~/ui/fakebooks";
-
-export const handle = { forceDark: true };
-
-export default function RoutingIndex() {
- return (
-
- );
-}
diff --git a/test-apps/remix-website/app/routes/[_]docs.routing.tsx b/test-apps/remix-website/app/routes/[_]docs.routing.tsx
deleted file mode 100644
index 5794e44..0000000
--- a/test-apps/remix-website/app/routes/[_]docs.routing.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { InteractiveRoutes } from "~/ui/homepage-scroll-experience";
-
-export const handle = { forceDark: true };
-
-export default function Routing() {
- return (
-
-
-
- );
-}
diff --git a/test-apps/remix-website/app/routes/_extras.blog.$slug.tsx b/test-apps/remix-website/app/routes/_extras.blog.$slug.tsx
deleted file mode 100644
index 2256807..0000000
--- a/test-apps/remix-website/app/routes/_extras.blog.$slug.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-import type { HeadersFunction, LoaderFunctionArgs } from "@remix-run/node";
-import { useLoaderData } from "@remix-run/react";
-import type { MetaFunction } from "@remix-run/react";
-import { json } from "@remix-run/node";
-import invariant from "tiny-invariant";
-
-import { getBlogPost } from "~/lib/blog.server";
-import "~/styles/md.css";
-import { useRef } from "react";
-import { useDelegatedReactRouterLinks } from "~/ui/delegate-links";
-import { CACHE_CONTROL } from "~/lib/http.server";
-import { Subscribe } from "~/ui/subscribe";
-
-export const loader = async ({ params, request }: LoaderFunctionArgs) => {
- let { slug } = params;
- invariant(!!slug, "Expected slug param");
- let requestUrl = new URL(request.url);
- let siteUrl = requestUrl.protocol + "//" + requestUrl.host;
-
- let post = await getBlogPost(slug);
- return json(
- { siteUrl, post },
- { headers: { "Cache-Control": CACHE_CONTROL.DEFAULT } },
- );
-};
-
-export const headers: HeadersFunction = ({ loaderHeaders }) => {
- // Inherit the caching headers from the loader so we do't cache 404s
- return loaderHeaders;
-};
-
-export const meta: MetaFunction = (args) => {
- let { data, params } = args;
- let { slug } = params;
- invariant(!!slug, "Expected slug param");
-
- let { siteUrl, post } = data || {};
- if (!post) {
- return [{ title: "404 Not Found | Remix" }];
- }
-
- let ogImageUrl = siteUrl ? new URL(`${siteUrl}/img/${slug}`) : null;
- if (ogImageUrl) {
- ogImageUrl.searchParams.set("title", post.title);
- ogImageUrl.searchParams.set("date", post.dateDisplay);
- for (let { name, title } of post.authors) {
- ogImageUrl.searchParams.append("authorName", name);
- ogImageUrl.searchParams.append("authorTitle", title);
- }
- }
-
- let socialImageUrl = ogImageUrl?.toString();
- let url = siteUrl ? `${siteUrl}/blog/${slug}` : null;
-
- return [
- { title: post.title + " | Remix" },
- { name: "description", content: post.summary },
- { property: "og:url", content: url },
- { property: "og:title", content: post.title },
- { property: "og:image", content: socialImageUrl },
- { property: "og:description", content: post.summary },
- { name: "twitter:card", content: "summary_large_image" },
- { name: "twitter:creator", content: "@remix_run" },
- { name: "twitter:site", content: "@remix_run" },
- { name: "twitter:title", content: post.title },
- { name: "twitter:description", content: post.summary },
- { name: "twitter:image", content: socialImageUrl },
- {
- name: "twitter:image:alt",
- content: socialImageUrl ? post.imageAlt : undefined,
- },
- ];
-};
-
-export default function BlogPost() {
- let { post } = useLoaderData();
- let mdRef = useRef(null);
- useDelegatedReactRouterLinks(mdRef);
-
- return (
- <>
- {post.draft ? (
-
- 🚨 This is a draft, please do not share this page until it's
- officially published 🚨
-
- ) : null}
-
-
-
-
-
-
-
-
-
-
- {post.dateDisplay}
-
-
-
- {post.title}
-
-
-
-
- {post.authors.map((author) => (
-
-
-
-
-
-
-
- {author.name}
-
-
- {author.title}
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
- Get updates on the latest Remix news
-
-
- Be the first to learn about new Remix features, community events, and
- tutorials.
-
-
-
- >
- );
-}
diff --git a/test-apps/remix-website/app/routes/_extras.blog._index.tsx b/test-apps/remix-website/app/routes/_extras.blog._index.tsx
deleted file mode 100644
index 6e3c3af..0000000
--- a/test-apps/remix-website/app/routes/_extras.blog._index.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import * as React from "react";
-import type { LoaderFunctionArgs } from "@remix-run/node";
-import { useLoaderData, Link } from "@remix-run/react";
-import type { MetaFunction } from "@remix-run/react";
-import { json } from "@remix-run/node";
-import { Subscribe } from "~/ui/subscribe";
-import { CACHE_CONTROL } from "~/lib/http.server";
-import { getBlogPostListings } from "~/lib/blog.server";
-
-export const loader = async (_: LoaderFunctionArgs) => {
- return json(
- { posts: await getBlogPostListings() },
- { headers: { "Cache-Control": CACHE_CONTROL.DEFAULT } },
- );
-};
-
-export const meta: MetaFunction = () => {
- return [
- { title: "Remix Blog" },
- {
- name: "description",
- content: "Thoughts about building excellent user experiences with Remix.",
- },
- ];
-};
-
-export default function Blog() {
- const data = useLoaderData();
- const [latestPost, ...posts] = data.posts;
-
- let featuredPosts = data.posts.filter((post) => post.featured);
-
- return (
-
-
-
-
-
-
-
-
-
{latestPost.dateDisplay}
-
- {latestPost.title}
-
-
{latestPost.summary}
-
-
-
- {posts.map((post) => (
-
-
-
-
-
-
{post.dateDisplay}
-
{post.title}
-
{post.summary}
-
-
- ))}
-
-
-
-
- {featuredPosts.length ? (
- <>
-
- Featured Articles
-
-
- {featuredPosts.map((post, index, array) => (
-
-
- {index !== array.length - 1 && }
-
- ))}
-
-
- >
- ) : null}
-
-
- Get updates on the latest Remix news
-
-
- Be the first to learn about new Remix features, community events,
- and tutorials.
-
-
-
-
-
-
- );
-}
diff --git a/test-apps/remix-website/app/routes/_extras.blog.rss[.xml].tsx b/test-apps/remix-website/app/routes/_extras.blog.rss[.xml].tsx
deleted file mode 100644
index f8e89df..0000000
--- a/test-apps/remix-website/app/routes/_extras.blog.rss[.xml].tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import type { LoaderFunction } from "@remix-run/node";
-import { getBlogPostListings } from "~/lib/blog.server";
-import { Feed } from "feed";
-import { CACHE_CONTROL } from "~/lib/http.server";
-
-export const loader: LoaderFunction = async () => {
- const blogUrl = `https://remix.run/blog`;
- const posts = await getBlogPostListings();
-
- const feed = new Feed({
- id: blogUrl,
- title: "Remix Blog",
- description:
- "Thoughts about building excellent user experiences with Remix.",
- link: blogUrl,
- language: "en",
- updated: posts.length > 0 ? new Date(posts[0].dateDisplay) : new Date(),
- generator: "https://github.com/jpmonette/feed",
- copyright: "© Shopify, Inc.",
- });
-
- posts.forEach((post) => {
- const postLink = `${blogUrl}/${post.slug}`;
- feed.addItem({
- id: postLink,
- title: post.title,
- link: postLink,
- date: new Date(post.dateDisplay),
- description: post.summary,
- });
- });
-
- return new Response(feed.rss2(), {
- headers: {
- "Content-Type": "application/xml",
- "Cache-Control": CACHE_CONTROL.DEFAULT,
- },
- });
-};
diff --git a/test-apps/remix-website/app/routes/_extras.resources.$slug.tsx b/test-apps/remix-website/app/routes/_extras.resources.$slug.tsx
deleted file mode 100644
index 0dec870..0000000
--- a/test-apps/remix-website/app/routes/_extras.resources.$slug.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-// Pull full readme for this page from GitHub
-import {
- json,
- type LoaderFunctionArgs,
- type HeadersFunction,
- type MetaFunction,
-} from "@remix-run/node";
-import { useLoaderData } from "@remix-run/react";
-import invariant from "tiny-invariant";
-import { getResource } from "~/lib/resources.server";
-import { InitCodeblock, ResourceTag, useCreateTagUrl } from "~/ui/resources";
-import { octokit } from "~/lib/github.server";
-import "~/styles/docs.css";
-import iconsHref from "~/icons.svg";
-import { CACHE_CONTROL } from "~/lib/http.server";
-
-export async function loader({ request, params }: LoaderFunctionArgs) {
- const resourceSlug = params.slug;
- invariant(resourceSlug, "resourceSlug is required");
-
- let resource = await getResource(resourceSlug, { octokit });
-
- if (!resource) {
- throw json({}, { status: 404 });
- }
-
- let requestUrl = new URL(request.url);
- let siteUrl = requestUrl.protocol + "//" + requestUrl.host;
-
- return json(
- {
- siteUrl,
- resource,
- },
- { headers: { "Cache-Control": CACHE_CONTROL.DEFAULT } },
- );
-}
-
-export const headers: HeadersFunction = ({ loaderHeaders }) => {
- return loaderHeaders;
-};
-
-export const meta: MetaFunction = (args) => {
- let { data, params } = args;
- let { slug } = params;
- invariant(!!slug, "Expected slug param");
-
- let { siteUrl, resource } = data || {};
- if (!resource) {
- return [{ title: "404 Not Found | Remix" }];
- }
-
- let socialImageUrl = resource.imgSrc;
- let url = siteUrl ? `${siteUrl}/blog/${slug}` : null;
-
- return [
- { title: resource.title + " | Remix Resources" },
- { name: "description", content: resource.description },
- { property: "og:url", content: url },
- { property: "og:title", content: resource.title },
- { property: "og:image", content: socialImageUrl },
- { property: "og:description", content: resource.description },
- { name: "twitter:card", content: "summary_large_image" },
- { name: "twitter:creator", content: "@remix_run" },
- { name: "twitter:site", content: "@remix_run" },
- { name: "twitter:title", content: resource.title },
- { name: "twitter:description", content: resource.description },
- { name: "twitter:image", content: socialImageUrl },
- ];
-};
-
-export default function ResourcePage() {
- let { resource } = useLoaderData();
- let {
- description,
- repoUrl,
- initCommand,
- sponsorUrl,
- starsFormatted,
- tags,
- readmeHtml,
- } = resource;
- let createTagUrl = useCreateTagUrl();
-
- return (
-
-
- {/* The sidebar comes first with a flex row-reverse for better keyboard navigation */}
-
-
-
-
- {readmeHtml ? (
-
- ) : null}
-
-
- );
-}
diff --git a/test-apps/remix-website/app/routes/_extras.resources._index/data.server.ts b/test-apps/remix-website/app/routes/_extras.resources._index/data.server.ts
deleted file mode 100644
index 48b69d9..0000000
--- a/test-apps/remix-website/app/routes/_extras.resources._index/data.server.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import {
- getAllResources,
- type Category,
- type Resource,
-} from "~/lib/resources.server";
-import { categories } from "./ui";
-import { redirect } from "@remix-run/node";
-import { octokit } from "~/lib/github.server";
-
-export async function getResourcesForRequest(request: Request) {
- let allResources = await getAllResources({ octokit });
- let { selectedCategory, selectedTags } = checkSearchParams(
- request,
- allResources,
- );
-
- // filter resources by category -- need to do it after we check the search params since we need all of the valid tags for that
- let resources =
- selectedCategory === "all"
- ? allResources
- : allResources.filter(({ category }) => category === selectedCategory);
-
- // show the featured resource if no tags are selected
- if (selectedTags.length === 0) {
- let featuredIdx = allResources.findIndex(({ featured }) => featured);
- featuredIdx = featuredIdx === -1 ? 0 : featuredIdx;
-
- let featuredResource = allResources[featuredIdx];
- allResources.splice(featuredIdx, 1);
-
- return {
- selectedCategory,
- selectedTags,
- featuredResource,
- resources,
- };
- }
-
- // get all resources that have all of the selected tags
- resources = resources.filter(({ tags }) => {
- return selectedTags.every((tag) => tags.includes(tag));
- });
-
- return {
- selectedCategory,
- selectedTags,
- resources,
- featuredResource: null,
- };
-}
-
-/**
- * Checks that the category and selected tags are all valid
- * Throws a redirect if the category is missing or invalid or if
- * any of the selected tags are invalid
- */
-function checkSearchParams(request: Request, resources: Resource[]) {
- let hasInvalidTag = false;
- // get search params: category and tags
- let searchParams = new URL(request.url).searchParams;
- let selectedCategory = searchParams.get("category");
- let selectedTagsSet = new Set(searchParams.getAll("tag"));
- let tags = new Set(resources.flatMap(({ tags }) => tags));
-
- // handle a missing or incorrect category
- if (!selectedCategory || !categories.includes(selectedCategory)) {
- searchParams.set("category", "all");
- hasInvalidTag = true;
- }
-
- // filter by selected tags and return both the selected tags and the filtered resources
- let selectedTags = [...selectedTagsSet];
-
- // drop all tags that
- for (let tag of selectedTags) {
- if (tags.has(tag)) continue;
- searchParams.delete("tag", tag);
- hasInvalidTag = true;
- }
-
- if (hasInvalidTag) {
- throw redirect(`/resources?${searchParams}`);
- }
-
- return {
- // the check above assures that the selectedCategory is the right type
- selectedCategory: selectedCategory as Category,
- selectedTags,
- };
-}
diff --git a/test-apps/remix-website/app/routes/_extras.resources._index/route.tsx b/test-apps/remix-website/app/routes/_extras.resources._index/route.tsx
deleted file mode 100644
index 31e3e19..0000000
--- a/test-apps/remix-website/app/routes/_extras.resources._index/route.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import {
- json,
- type LoaderFunctionArgs,
- type HeadersFunction,
- type MetaFunction,
-} from "@remix-run/node";
-import { useLoaderData } from "@remix-run/react";
-import { ResourceTag, useCreateTagUrl } from "~/ui/resources";
-import { getResourcesForRequest } from "./data.server";
-import { CACHE_CONTROL } from "~/lib/http.server";
-import {
- FeaturedResourcePoster,
- ResourceCards,
- ResourceCategoryTabs,
-} from "./ui";
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
- let requestUrl = new URL(request.url);
- let siteUrl = requestUrl.protocol + "//" + requestUrl.host;
- let data = await getResourcesForRequest(request);
-
- return json(
- { ...data, siteUrl },
- { headers: { "Cache-Control": CACHE_CONTROL.DEFAULT } },
- );
-};
-
-export const headers: HeadersFunction = ({ loaderHeaders }) => {
- return loaderHeaders;
-};
-
-export const meta: MetaFunction = (args) => {
- let { siteUrl } = args.data || {};
- let title = "Remix Resources";
- let image = siteUrl ? `${siteUrl}/img/og.1.jpg` : null;
- let description = "Remix Resources made by the community, for the community";
-
- return [
- { title },
- { name: "description", content: description },
- { property: "og:url", content: `${siteUrl}/showcase` },
- { property: "og:title", content: title },
- { property: "og:description", content: description },
- { property: "og:image", content: image },
- { name: "twitter:card", content: "summary_large_image" },
- { name: "twitter:creator", content: "@remix_run" },
- { name: "twitter:site", content: "@remix_run" },
- { name: "twitter:title", content: title },
- { name: "twitter:description", content: description },
- { name: "twitter:image", content: image },
- ];
-};
-
-export default function Resources() {
- let { featuredResource, selectedCategory, selectedTags, resources } =
- useLoaderData();
- let createTagUrl = useCreateTagUrl();
-
- return (
-
- {featuredResource ? (
-
-
- Remix Resources
-
-
- Made by the community, for the community
-
-
- ) : (
-
-
- Resources that use
-
-
- {selectedTags.map((tag) => (
-
- {tag}
-
- ))}
-
-
- )}
-
-
- {featuredResource ? (
-
- ) : null}
-
-
-
-
-
-
- );
-}
diff --git a/test-apps/remix-website/app/routes/_extras.resources._index/ui.tsx b/test-apps/remix-website/app/routes/_extras.resources._index/ui.tsx
deleted file mode 100644
index c2c4599..0000000
--- a/test-apps/remix-website/app/routes/_extras.resources._index/ui.tsx
+++ /dev/null
@@ -1,261 +0,0 @@
-import type { Category, Resource } from "~/lib/resources.server";
-import { InitCodeblock, ResourceTag, useCreateTagUrl } from "~/ui/resources";
-import { Link, useSearchParams } from "@remix-run/react";
-import cx from "clsx";
-import iconsHref from "~/icons.svg";
-import { slugify } from "~/ui/primitives/utils";
-
-export let categories = ["all", "templates", "libraries"];
-
-export function FeaturedResourcePoster({
- featuredResource,
-}: {
- featuredResource: Resource;
-}) {
- let {
- title,
- description,
- imgSrc,
- repoUrl,
- starsFormatted,
- initCommand,
- sponsorUrl,
- tags,
- } = featuredResource;
- let createTagUrl = useCreateTagUrl();
-
- return (
- <>
-
-
-
-
- Featured
-
- {title}
-
-
- {description}
-
-
- {tags.map((tag) => (
-
- {tag}
-
- ))}
-
-
- >
- );
-}
-
-export function ResourceCategoryTabs({
- selectedCategory,
-}: {
- selectedCategory: Category;
-}) {
- let [searchParams] = useSearchParams();
-
- let tabs = categories.map((category) => {
- let newSearchParams = new URLSearchParams(searchParams);
- newSearchParams.set("category", category);
-
- return {
- name: category,
- href: `?${newSearchParams.toString()}`,
- current: category === selectedCategory,
- };
- });
-
- return (
-
-
-
-
- {tabs.map((tab) => (
-
- {tab.name}
-
- ))}
-
-
-
-
- );
-}
-
-type ResourceCardsProps = {
- resources: Resource[];
- selectedCategory: string;
- selectedTags: string[];
-};
-export function ResourceCards({
- resources,
- selectedCategory,
- selectedTags,
-}: ResourceCardsProps) {
- let createTagUrl = useCreateTagUrl();
- let selectedTagsSet = new Set(selectedTags);
-
- if (resources.length > 0) {
- return resources.map(({ title, description, tags, ...props }) => (
-
-
-
- {title}
-
- {description ? (
-
- {description}
-
- ) : null}
-
- {tags.map((tag) => (
-
- {tag}
-
- ))}
-
-
- ));
- }
-
- return (
-
- No{" "}
-
- {selectedCategory === "all" ? "resources" : selectedCategory}
- {" "}
- found with this combination of tags.{" "}
- Try removing some of the tags.
-
- );
-}
-
-type ResourcePosterProps = Pick<
- Resource,
- "imgSrc" | "repoUrl" | "starsFormatted" | "initCommand" | "sponsorUrl"
-> & {
- /** make the poster a link */
- to?: string;
- className?: string;
-};
-
-function ResourcePoster({
- to,
- imgSrc,
- repoUrl,
- starsFormatted,
- initCommand,
- sponsorUrl,
- className,
-}: ResourcePosterProps) {
- let img = (
-
- );
-
- return (
-
-
-
- {to ? (
-
- {img}
-
- ) : (
- img
- )}
-
-
-
-
-
- );
-}
-
-function GitHubLinks({
- repoUrl,
- starsFormatted,
- sponsorUrl,
-}: Pick) {
- return (
-
- );
-}
diff --git a/test-apps/remix-website/app/routes/_extras.showcase.tsx b/test-apps/remix-website/app/routes/_extras.showcase.tsx
deleted file mode 100644
index c5013ce..0000000
--- a/test-apps/remix-website/app/routes/_extras.showcase.tsx
+++ /dev/null
@@ -1,276 +0,0 @@
-import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
-import { json } from "@remix-run/node";
-import { useLoaderData } from "@remix-run/react";
-import { Fragment, forwardRef, useEffect, useRef } from "react";
-import type { ShowcaseExample } from "~/lib/showcase.server";
-import { showcaseExamples } from "~/lib/showcase.server";
-import { clsx } from "clsx";
-import { CACHE_CONTROL } from "~/lib/http.server";
-import { useHydrated } from "~/ui/primitives/utils";
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
- let requestUrl = new URL(request.url);
- let siteUrl = requestUrl.protocol + "//" + requestUrl.host;
-
- return json(
- { siteUrl, showcaseExamples },
- { headers: { "Cache-Control": CACHE_CONTROL.DEFAULT } },
- );
-};
-
-// Stolen from _marketing._index.tsx. eventually would like to replace
-export const meta: MetaFunction = (args) => {
- let { siteUrl } = args.data || {};
- let title = "Remix Showcase";
- let image = siteUrl ? `${siteUrl}/img/og.1.jpg` : null;
- let description = "See who is using Remix to build better websites.";
-
- return [
- { title },
- { name: "description", content: description },
- { property: "og:url", content: `${siteUrl}/showcase` },
- { property: "og:title", content: title },
- { property: "og:description", content: description },
- { property: "og:image", content: image },
- { name: "twitter:card", content: "summary_large_image" },
- { name: "twitter:creator", content: "@remix_run" },
- { name: "twitter:site", content: "@remix_run" },
- { name: "twitter:title", content: title },
- { name: "twitter:description", content: description },
- { name: "twitter:image", content: image },
- ];
-};
-
-export default function Showcase() {
- let { showcaseExamples } = useLoaderData();
- // Might be a bit silly to declare here and then prop-drill, but was a little concerned about a needless useEffect+useState for every card
- let isHydrated = useHydrated();
-
- return (
-
-
-
- Remix Showcase
-
-
- Checkout the companies, organizations, nonprofits, and indie
- developers building better websites with Remix
-
-
-
- {showcaseExamples.map((example, i) => {
- let preload: ShowcaseTypes["preload"] = i < 6 ? "auto" : "none";
- return (
-
-
- 5}
- // Only preload the first 2, since that's about all that should be "above the fold" on mobile
- preload={i < 2 ? "auto" : "none"}
- {...example}
- />
-
- );
- })}
-
-
- );
-}
-
-type ShowcaseTypes = ShowcaseExample & {
- preload?: "auto" | "metadata" | "none";
- isHydrated: boolean;
-};
-
-function DesktopShowcase({
- name,
- description,
- link,
- imgSrc,
- videoSrc,
- preload,
- isHydrated,
-}: ShowcaseTypes) {
- let videoRef = useRef(null);
-
- return (
-
-
- videoRef.current?.play()}
- pauseVideo={() => videoRef.current?.pause()}
- />
-
- );
-}
-
-function MobileShowcase({
- name,
- description,
- link,
- imgSrc,
- videoSrc,
- asImage = false,
- isHydrated,
- preload,
-}: ShowcaseTypes & {
- /** Opt into only showing an image. This avoids loading a ton of videos on the page and crashing Brooks' terrible phone */
- asImage?: boolean;
-}) {
- const ref = useRef(null);
-
- // autoplay videos -- using this useEffect because the `autoPlay` attribute overrides
- // `preload="none"` which causes weird infinite loading issues when caching is turned off
- useEffect(() => {
- const node = ref.current;
- if (!node) return;
-
- if (node.paused) {
- node.play();
- }
- }, []);
-
- return (
-
- {/* If it's an image, don't render the video, and remove the motion-safe class */}
- {!asImage ? (
-
- ) : null}
- {/* prefers-reduced-motion displays just an image even when there's a video */}
-
-
-
- );
-}
-
-let ShowcaseVideo = forwardRef<
- HTMLVideoElement,
- Pick &
- React.VideoHTMLAttributes
->(({ videoSrc, className, isHydrated, ...props }, ref) => {
- return (
-
-
- {["webm", "mp4"].map((ext) => (
-
- ))}
-
-
- );
-});
-
-function ShowcaseImage({
- className,
- ...props
-}: React.ImgHTMLAttributes) {
- return (
-
-
-
- );
-}
-
-function ShowcaseDescription({
- description,
- link,
- name,
- isHydrated,
- playVideo,
- pauseVideo,
-}: Pick & {
- playVideo?: () => void;
- pauseVideo?: () => void;
-}) {
- return (
-
-