From 3ec37ba591f52376f21981c91e536ac7d78c37e2 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf Date: Mon, 17 Jul 2023 10:05:46 +0530 Subject: [PATCH] async getRoutes and tanstack app router example --- .../react-spa-tanstack-router-app/.gitignore | 1 + examples/react-spa-tanstack-router-app/app.js | 120 ++++++++++ .../app/client.tsx | 219 ++++++++++++++++++ .../app/error.tsx} | 0 .../app/routes/hello.css | 3 + .../app/routes/hello/page.tsx | 12 + .../app/routes/hello/white.css | 3 + .../app/routes/page.tsx | 38 +++ .../app/routes/posts/[postId]/page.tsx | 48 ++++ .../app/routes/posts/layout.tsx | 52 +++++ .../app/routes/posts/page.tsx | 3 + .../app/style.css | 4 + .../react-spa-tanstack-router-app/index.html | 13 ++ .../package.json | 33 +++ .../postcss.config.cjs | 6 + .../public/favicon.ico | Bin 0 -> 370070 bytes .../tailwind.config.cjs | 8 + .../tsconfig.json | 15 ++ examples/react-spa-tanstack-router/app.js | 57 +++-- .../react-spa-tanstack-router/app/client.tsx | 3 +- .../react-spa-tanstack-router/app/error.tsx | 1 + .../app/pages/posts/[postId].tsx | 13 +- .../react-spa-tanstack-router/package.json | 3 + examples/solid-router/app.js | 4 + packages/vinxi-solid/lazy-route.jsx | 1 - packages/vinxi/lib/app.js | 1 - packages/vinxi/lib/build.js | 11 +- packages/vinxi/lib/dev-server.js | 19 +- packages/vinxi/lib/file-system-router.js | 10 +- .../lib/manifest/prod-server-manifest.js | 2 +- packages/vinxi/lib/manifest/spa-manifest.js | 9 +- packages/vinxi/lib/plugins/config.js | 4 +- packages/vinxi/lib/plugins/routes.js | 81 +++---- .../vinxi/lib/plugins/tree-shake.babel.js | 7 - 34 files changed, 712 insertions(+), 92 deletions(-) create mode 100644 examples/react-spa-tanstack-router-app/.gitignore create mode 100644 examples/react-spa-tanstack-router-app/app.js create mode 100644 examples/react-spa-tanstack-router-app/app/client.tsx rename examples/{react-spa-tanstack-router/app/NotFoundError.tsx => react-spa-tanstack-router-app/app/error.tsx} (100%) create mode 100644 examples/react-spa-tanstack-router-app/app/routes/hello.css create mode 100644 examples/react-spa-tanstack-router-app/app/routes/hello/page.tsx create mode 100644 examples/react-spa-tanstack-router-app/app/routes/hello/white.css create mode 100644 examples/react-spa-tanstack-router-app/app/routes/page.tsx create mode 100644 examples/react-spa-tanstack-router-app/app/routes/posts/[postId]/page.tsx create mode 100644 examples/react-spa-tanstack-router-app/app/routes/posts/layout.tsx create mode 100644 examples/react-spa-tanstack-router-app/app/routes/posts/page.tsx create mode 100644 examples/react-spa-tanstack-router-app/app/style.css create mode 100644 examples/react-spa-tanstack-router-app/index.html create mode 100644 examples/react-spa-tanstack-router-app/package.json create mode 100644 examples/react-spa-tanstack-router-app/postcss.config.cjs create mode 100644 examples/react-spa-tanstack-router-app/public/favicon.ico create mode 100644 examples/react-spa-tanstack-router-app/tailwind.config.cjs create mode 100644 examples/react-spa-tanstack-router-app/tsconfig.json create mode 100644 examples/react-spa-tanstack-router/app/error.tsx diff --git a/examples/react-spa-tanstack-router-app/.gitignore b/examples/react-spa-tanstack-router-app/.gitignore new file mode 100644 index 00000000..e985853e --- /dev/null +++ b/examples/react-spa-tanstack-router-app/.gitignore @@ -0,0 +1 @@ +.vercel diff --git a/examples/react-spa-tanstack-router-app/app.js b/examples/react-spa-tanstack-router-app/app.js new file mode 100644 index 00000000..4a988b1c --- /dev/null +++ b/examples/react-spa-tanstack-router-app/app.js @@ -0,0 +1,120 @@ +import reactRefresh from "@vitejs/plugin-react"; +import { init, parse } from "es-module-lexer"; +import esbuild from "esbuild"; +import fs from "fs"; +import { join } from "path"; +import { createApp } from "vinxi"; +import { glob, pathToRegexp } from "vinxi/file-system-router"; + +class TanstackFileSystemRouter { + routes; + constructor(config) { + this.config = config; + } + + async buildRoutes() { + function toPath(path, config) { + const routePath = path + .slice(config.dir.length) + .replace(/\.(ts|tsx|js|jsx)$/, "") + .replace(/\/page$/, "/") + .replace(/\/layout$/, "") + .replace(/index$/, "") + .replace(/\[([^\/]+)\]/g, (_, m) => { + if (m.length > 3 && m.startsWith("...")) { + return `*${m.slice(3)}`; + } + if (m.length > 2 && m.startsWith("[") && m.endsWith("]")) { + return `$${m.slice(1, -1)}?`; + } + return `$${m}`; + }); + + return routePath?.length > 0 ? `${routePath}` : "/"; + } + await init; + this.routes = [ + ...glob(join(this.config.dir, "**/(page|layout).tsx"), { + absolute: true, + }).map((src) => { + console.log(src); + let path = toPath(src, this.config); + let keys = []; + let regex = pathToRegexp(path, keys); + + const [_, exports] = parse( + esbuild.transformSync(fs.readFileSync(src, "utf-8"), { + jsx: "transform", + format: "esm", + loader: "tsx", + }).code, + src, + ); + const hasLoader = exports.find((e) => e.n === "loader"); + const hasErrorBoundary = exports.find((e) => e.n === "ErrorBoundary"); + const hasLoading = exports.find((e) => e.n === "Loading"); + const hasConfig = exports.find((e) => e.n === "config"); + return { + regex, + keys, + $component: { + src: src, + pick: ["default", "$css"], + }, + $error: hasErrorBoundary + ? { src: src, pick: ["ErrorBoundary"] } + : undefined, + $loading: hasLoading ? { src: src, pick: ["Loading"] } : undefined, + $$loader: hasLoader + ? { + src: src, + pick: ["loader"], + } + : undefined, + $$config: hasConfig ? { src: src, pick: ["config"] } : undefined, + path, + filePath: src, + }; + }), + ]; + + return this.routes; + } + + buildRoutesPromise = undefined; + + async getRoutes() { + if (!this.buildRoutesPromise) { + this.buildRoutesPromise = this.buildRoutes(); + } + return await this.buildRoutesPromise; + } +} + +export default createApp({ + routers: [ + { + name: "public", + mode: "static", + dir: "./public", + }, + { + name: "client", + mode: "spa", + handler: "./index.html", + dir: "./app/routes", + root: "./app/root.tsx", + style: TanstackFileSystemRouter, + build: { + babel: { + plugins: [ + "@babel/plugin-syntax-jsx", + ["@babel/plugin-syntax-typescript", { isTSX: true }], + ], + }, + target: "browser", + plugins: () => [reactRefresh()], + }, + }, + ], +}); diff --git a/examples/react-spa-tanstack-router-app/app/client.tsx b/examples/react-spa-tanstack-router-app/app/client.tsx new file mode 100644 index 00000000..d6e2517e --- /dev/null +++ b/examples/react-spa-tanstack-router-app/app/client.tsx @@ -0,0 +1,219 @@ +/// +import { + Loader, + LoaderClient, + LoaderClientProvider, +} from "@tanstack/react-loaders"; +import { + Link, + Outlet, + RootRoute, + Route, + Router, + RouterProvider, +} from "@tanstack/router"; +import { TanStackRouterDevtools } from "@tanstack/router-devtools"; +import { lazyRoute } from "@vinxi/react"; +import axios from "axios"; +import { StrictMode } from "react"; +import ReactDOM from "react-dom/client"; +import fileRoutes from "vinxi/routes"; +import "vinxi/runtime/client"; + +import { NotFoundError } from "./error"; +import "./style.css"; + +type PostType = { + id: string; + title: string; + body: string; +}; + +const fetchPosts = async () => { + console.log("Fetching posts..."); + await new Promise((r) => setTimeout(r, 500)); + return axios + .get("https://jsonplaceholder.typicode.com/posts") + .then((r) => r.data.slice(0, 10)); +}; + +const fetchPost = async (postId: string) => { + console.log(`Fetching post with id ${postId}...`); + await new Promise((r) => setTimeout(r, 500)); + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((e) => { + if (e.response.status === 404) { + return null; + } + throw e; + }); + + if (!post) { + throw new NotFoundError(`Post with id "${postId}" not found!`); + } + + return post; +}; + +const postsLoader = new Loader({ + fn: fetchPosts, +}); + +const postLoader = new Loader({ + fn: fetchPost, + onInvalidate: () => { + postsLoader.invalidate(); + }, +}); + +const loaderClient = new LoaderClient({ + getLoaders: () => ({ + posts: postsLoader, + post: postLoader, + }), +}); + +declare module "@tanstack/react-loaders" { + interface Register { + loaderClient: typeof loaderClient; + } +} + +type RouterContext = { + loaderClient: typeof loaderClient; +}; + +const rootRoute = RootRoute.withRouterContext()({ + component: () => { + return ( + <> +
+ + Home + {" "} + + Posts + +
+
+ + {/* Start rendering router matches */} + + + ); + }, +}); + +const defineRoutes = (fileRoutes) => { + function processRoute(routes, route, id, full) { + const parentRoute = Object.values(routes).find((o) => { + // if (o.id.endsWith("/index")) { + // return false; + // } + return id.startsWith(o.path + "/"); + }); + + if (!parentRoute) { + routes.push({ ...route, path: id }); + return routes; + } + processRoute( + parentRoute.children || (parentRoute.children = []), + route, + id.slice(parentRoute.path.length), + full, + ); + + return routes; + } + + return fileRoutes + .sort((a, b) => a.path.length - b.path.length) + .reduce((prevRoutes, route) => { + return processRoute(prevRoutes, route, route.path, route.path); + }, []); +}; + +const routes = defineRoutes(fileRoutes); +console.log(routes); + +function createRoute(route, parent) { + const parentRoute = new Route({ + path: route.path, + component: lazyRoute(route.$component, import.meta.env.MANIFEST["client"]), + errorComponent: route.$error + ? lazyRoute( + route.$error, + import.meta.env.MANIFEST["client"], + undefined, + "ErrorBoundary", + ) + : undefined, + pendingComponent: route.$loading + ? lazyRoute( + route.$loading, + import.meta.env.MANIFEST["client"], + undefined, + "Loading", + ) + : undefined, + loader: route.$$loader?.require().loader, + ...(route.$$config?.require().config ?? {}), + getParentRoute: () => parent, + }); + if (route.children) { + parentRoute.addChildren( + route.children.map((route) => createRoute(route, parentRoute)), + ); + } + return parentRoute; +} + +const routeTree = rootRoute.addChildren([ + ...routes.map((route) => { + return createRoute(route, rootRoute); + }), +]); + +// Set up a Router instance +const router = new Router({ + routeTree, + defaultPreload: "intent", + context: { + loaderClient, + }, +}); + +// Register things for typesafety +declare module "@tanstack/router" { + interface Register { + router: typeof router; + } +} + +const rootElement = document.getElementById("root")!; + +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement); + + root.render( + + + + + , + ); +} diff --git a/examples/react-spa-tanstack-router/app/NotFoundError.tsx b/examples/react-spa-tanstack-router-app/app/error.tsx similarity index 100% rename from examples/react-spa-tanstack-router/app/NotFoundError.tsx rename to examples/react-spa-tanstack-router-app/app/error.tsx diff --git a/examples/react-spa-tanstack-router-app/app/routes/hello.css b/examples/react-spa-tanstack-router-app/app/routes/hello.css new file mode 100644 index 00000000..3293a956 --- /dev/null +++ b/examples/react-spa-tanstack-router-app/app/routes/hello.css @@ -0,0 +1,3 @@ +* { + background-color: purple; +} \ No newline at end of file diff --git a/examples/react-spa-tanstack-router-app/app/routes/hello/page.tsx b/examples/react-spa-tanstack-router-app/app/routes/hello/page.tsx new file mode 100644 index 00000000..a77bbe4b --- /dev/null +++ b/examples/react-spa-tanstack-router-app/app/routes/hello/page.tsx @@ -0,0 +1,12 @@ +import { Link } from "@tanstack/router"; +import React from "react"; + +import "./white.css"; + +export default function Hello() { + return ( +
+ Hello world Home 123 +
+ ); +} diff --git a/examples/react-spa-tanstack-router-app/app/routes/hello/white.css b/examples/react-spa-tanstack-router-app/app/routes/hello/white.css new file mode 100644 index 00000000..1cb6fca7 --- /dev/null +++ b/examples/react-spa-tanstack-router-app/app/routes/hello/white.css @@ -0,0 +1,3 @@ +html, body { + background-color: red; +} \ No newline at end of file diff --git a/examples/react-spa-tanstack-router-app/app/routes/page.tsx b/examples/react-spa-tanstack-router-app/app/routes/page.tsx new file mode 100644 index 00000000..b7e55bb1 --- /dev/null +++ b/examples/react-spa-tanstack-router-app/app/routes/page.tsx @@ -0,0 +1,38 @@ +import { Link } from "@tanstack/router"; +import React from "react"; + +import "./hello.css"; + +function Counter() { + const [count, setCount] = React.useState(0); + return ( +
+

Count: {count}

+ +
+ ); +} + +export default function Hello({ assets }) { + return ( + <> +
Hellsas\\dasd4
+ Hello + + {/*