diff --git a/.changeset/bright-glasses-watch.md b/.changeset/bright-glasses-watch.md new file mode 100644 index 00000000..147fc8e9 --- /dev/null +++ b/.changeset/bright-glasses-watch.md @@ -0,0 +1,6 @@ +--- +"@vinxi/server-functions": patch +"vinxi": patch +--- + +route rules for dev server diff --git a/docs/app.config.js b/docs/app.config.js index 4470a67e..4fd91876 100644 --- a/docs/app.config.js +++ b/docs/app.config.js @@ -145,14 +145,14 @@ function wouterFileRouter(config) { export default createApp({ server: { - // routeRules: { - // "**/*": { - // headers: { - // "Cross-Origin-Embedder-Policy": "require-corp", - // "Cross-Origin-Opener-Policy": "same-origin", - // }, - // }, - // }, + routeRules: { + "/**": { + headers: { + "Cross-Origin-Embedder-Policy": "require-corp", + "Cross-Origin-Opener-Policy": "same-origin", + }, + }, + }, }, routers: [ { diff --git a/docs/app/pages/index.md b/docs/app/pages/index.md index 341d7248..ab0d515c 100644 --- a/docs/app/pages/index.md +++ b/docs/app/pages/index.md @@ -37,10 +37,26 @@ The surface layer we are intending to tackle: ## Try it out -```bash + + +```bash pnpm +pnpm install vinxi +``` + +```bash npm npm install vinxi ``` +```bash yarn +yarn add vinxi +``` + +```bash bun +bun install vinxi +``` + + + ### React SSR ```ts @@ -58,7 +74,6 @@ export default createApp({ name: "client", mode: "build", handler: "./app/client.tsx", - target: "browser", plugins: () => [reactRefresh()], base: "/_build", }, @@ -66,7 +81,6 @@ export default createApp({ name: "ssr", mode: "handler", handler: "./app/server.tsx", - target: "server", }, ], }); @@ -89,7 +103,6 @@ export default createApp({ name: "client", mode: "build", handler: "./app/client.tsx", - target: "browser", plugins: () => [solid({ ssr: true })], base: "/_build", }, @@ -97,7 +110,6 @@ export default createApp({ name: "ssr", mode: "handler", handler: "./app/server.tsx", - target: "server", plugins: () => [solid({ ssr: true })], }, ], diff --git a/docs/app/pages/server-runtime.md b/docs/app/pages/server-runtime.mdx similarity index 98% rename from docs/app/pages/server-runtime.md rename to docs/app/pages/server-runtime.mdx index 5e3a2b34..37ce0605 100644 --- a/docs/app/pages/server-runtime.md +++ b/docs/app/pages/server-runtime.mdx @@ -1,3 +1,7 @@ +export const meta = { + title: "Server Runtime" +} + # Server Runtime Vinxi takes care of your server runtime needs. It's based on `unjs/h3`. Our philosophy is to abstract over all the different runtimes/platforms. Vinxi provides a set of helpers that covers all the usual use cases of the app to interact with the server platform. Apart from that it exposes the bare platform native objects for you to manipulate as necessary diff --git a/docs/app/router.tsx b/docs/app/router.tsx index c57c4d05..920979e8 100644 --- a/docs/app/router.tsx +++ b/docs/app/router.tsx @@ -53,7 +53,7 @@ function createRouter() { route.$component, import.meta.env.MANIFEST["client"], ), - + ...(route.$$meta ? { meta: route.$$meta } : {}), children: route.children?.map(createRoute), ...(route.$$loader ? { diff --git a/docs/content/0.index.md b/docs/content/0.index.md deleted file mode 100644 index 59ab6853..00000000 --- a/docs/content/0.index.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: My Docus project -navigation: false -layout: page ---- - -::block-hero ---- -cta: - - Get Started - - /guide -secondary: - - Open on GitHub → - - https://github.com/nksaraf/vinxi -snippet: npm install vinxi ---- - -#title -The App Framework - -#description -Develop universal full stack apps like its nothing. Build them optimally. Deploy them anywhere you feel like. -:: - -::card-grid -#title -What's included - -#root -:ellipsis - -#default - ::card - #title - Deploy Anywhere. - #description - Deploy your app to any platform, any cloud, any server. - :: - - ::card - #title - Web Standards Everywhere. - #description - Use the latest web standards and APIs to build your app. - :: - - ::card - #title - Bring your own UI Framework. - #description - Use any UI framework you like, or none at all. - :: - - ::card - #title - Hackable. - #description - Customize and extend the framework to your heart's content. - :: - - ::card - #title - Built on backs of giants. - #description - Use the best of the best in the JavaScript ecosystem. - :: - - ::card - #title - Vite/Rollup Plugin API. - #description - Use the Vite/Rollup plugin API and ecosystem to build your app. - :: - - ::card - #title - Bring your own opinions. - #description - Use the tools and libraries you like, or none at all. - :: - - ::card - #title - Hot Develepment Experience. - #description - Enjoy a blazing fast development experience. - :: - - ::card - #title - Ultra-fast Production Apps. - #description - Build apps that are fast by default. - :: -:: \ No newline at end of file diff --git a/docs/content/1.guide/0.index.md b/docs/content/1.guide/0.index.md deleted file mode 100644 index 3ac1c192..00000000 --- a/docs/content/1.guide/0.index.md +++ /dev/null @@ -1,26 +0,0 @@ -# Get started - -Let's get started with Docus. - -# Welcome to Docus - -Your new favorite way to build **documentation**. - -## How to use Docus ? - -Learn more on [docus.dev](https://docus.dev). - -::code-group - -```bash [npm] -npm install vinxi -``` - -```bash [yarn] -yarn add vinxi -``` - -```bash [pnpm] -pnpm add vinxi -``` -:: diff --git a/docs/content/1.guide/1.what-is-a-router.md b/docs/content/1.guide/1.what-is-a-router.md deleted file mode 100644 index ad8c0fcc..00000000 --- a/docs/content/1.guide/1.what-is-a-router.md +++ /dev/null @@ -1,184 +0,0 @@ -# What is a Router? - -Routers are the core primitive of Vinxi. - -A router is a specification for how a group of routes should be handled. Lets take a look at the different parts of a router. - -## Common Options - -### `name` - -:sandbox{src="https://stackblitz.com/github/nksaraf/vinxi/tree/main/examples/react/ssr/basic?file=app%2Fserver.tsx"} - -::list - -- Each router has a name. - -- Used to reference the router by other routers. For example, to access the manifest of a different router, use `import.meta.env.MANIFEST['']`. - -- The name of the router that the code is currently executing in is available as `import.meta.env. ROUTER_NAME`. -:: - -### `base` - -::list - -- The base path of the router. -- The path that the router will be mounted at. For example, if the base is `"/foo"`, then the router will be mounted at `"/foo"` and will respond to requests matching `"/foo/**/*"`. -- The base path is available as `import.meta.env.BASE_URL`. -:: - -### `routes` - -::list -- The mode of the router. -- Determines how the other parts of the router are interpreted. -- The mode is available as `import.meta.env.ROUTER_MODE`. -:: - -### `target` - -::list -- Target platform for the router. -- Available targets are: `"browser"`, `"server"` -:: - -### `plugins` -::list -- List of Vite/Rollup plugins to apply for the router. -:: - -### `outDir` - -::list -- The build output directory for the router. -- This is a temporary path because eventually the routers are served by the adapter. -:: - -### `mode` - -::list -- The mode of the router. -- Determines how the other parts of the router are interpreted. -- The mode is available as `import.meta.env.ROUTER_MODE`. -:: - -> Vinxi supports a few router modes out of the box: `"static"`, `"handler"`, `"build"`, `"spa"`. New modes can be added by you too. Lets see how each mode is different? - -## `mode: "static"` - -::list -- A simple static router that serves files from the `fs.dir` directory at the `base` path. -- Its useful for serving static assets like images, fonts, etc. -- Usually every app has one of these. -- No compilation is done for this router. -:: -```ts -import { createApp } from "vinxi"; - -export default createApp({ - routers: [ - name: "public", - base: "/", - mode: "static", - fs: { - dir: "./public", - }, - ] -}); -``` - -### `dir` - - -## `mode: "handler"` - -::list -- A router typically targetting the server. -- The router responds to requests matching the `base` path with the event handler exported from the `handler` file. -- It can also include a file system router to map files to sub routes -:: -```ts -import { createApp } from "vinxi"; - -export default createApp({ - routers: [ - name: "ssr", - base: "/", - mode: "handler", - handler: "./ssr-handler.ts", - target: "server" - ] -}); -``` - -### `handler` - -::list -- The mode of the router. -- Determines how the other parts of the router are interpreted. -- The mode is available as `import.meta.env.ROUTER_MODE`. -:: - -### `middleware` - -::list -- The mode of the router. -- Determines how the other parts of the router are interpreted. -- The mode is available as `import.meta.env.ROUTER_MODE`. -:: - -## mode: `"build"` - -::list -- A build router serves the compiled handler and subroutes as static assets. -- A common use case is to build the client-side of a SSR app. -- The chunks and entry-points exposed by a build router can be addressed using the manifest by other routers. -- Since its mostly built assets, its good to use a `base` that the user will not visit easily., eg. `"/_build"` -- Supports complete HMR on the client -:: - -```ts -import { createApp } from "vinxi"; - -export default createApp({ - routers: [ - name: "client", - base: "/_build", - mode: "build", - handler: "./client-handler.ts", - target: "client" - ] -}); -``` - -::list -- Use the handler from this router as the `src` to render a script tag in the SSR response. -:: - - - -```ts -const clientManifest = import.meta.env.MANIFEST["client"]; -const scriptSrc = clientManifest.inputs[clientManifest.handler]; -const scriptTag = ``; -``` - -### `handler` - -::list -- The mode of the router. -- Determines how the other parts of the router are interpreted. -- The mode is available as `import.meta.env.ROUTER_MODE`. -:: - - -## mode: `"spa"` - -### `handler` - -::list -- The mode of the router. -- Determines how the other parts of the router are interpreted. -- The mode is available as `import.meta.env.ROUTER_MODE`. -:: diff --git a/docs/content/1.guide/2.file-system-routing.md b/docs/content/1.guide/2.file-system-routing.md deleted file mode 100644 index 0ff4fc04..00000000 --- a/docs/content/1.guide/2.file-system-routing.md +++ /dev/null @@ -1,24 +0,0 @@ -# File System Routing - -As the number of sub-routes grows, it becomes tedious to write out each route by hand. Make sure to code split it, import it, include it in the main handler, etc. File system routing allows you to define routes in separate files and the handler can produce a route tree based on the file system structure and the names of the files. - -Unlike most file system routing solution, this one doesn't care what your preferred file system convention is. All it needs you is to tell how files would map to routes in that router. Vinxi will take care of everything from HMR for the routing config, to bundling them appropriately. - -The file system router is configured using two options: `fs.dir` and `fs.style`. The `fs.dir` option tells the router where to look for files. The `fs.style` option tells the router how to map files to routes. - -For the `fs.dir` option, you need to pass a class that implements the `FileSystemRouter` interface. The interface looks like this: - -```ts -class FileSystemRouter { - constructor(config: { dir: string }); - getRoutes(): Promise; - update(): Promise; -} -``` - -This is really abstract intentially. It allows you to design your own file system router. For example, you can use a router that reads from a database, or a router that reads from a remote server. The sky is the limit. - -Vinxi attaches an `update` listener to the file system router. When the router emits an `update` event, Vinxi will update the routing tree. This allows you to hot reload the routing config. It reloads the client too, so you can see the changes immediately. - -We export a utility class `BaseFileSystemRouter` that you can extend to create your own file system router. It's a good starting point if you want to create your own file system router that handles globbing, file watching, caching. All you need to do is implement `toPath` and `toRoute` methods. The `toPath` method takes a file path and returns a route path. The `toRoute` method takes a file path and returns a route object that is provided to the app using the `vinxi/routes` module. - diff --git a/docs/content/1.guide/3.manifests.md b/docs/content/1.guide/3.manifests.md deleted file mode 100644 index 1a33af55..00000000 --- a/docs/content/1.guide/3.manifests.md +++ /dev/null @@ -1,56 +0,0 @@ -# Manifests - -One of the trickier parts of making a full stack app is getting all the pieces to work together. This is where manifests come in. A manifest is a file that tells the runtime what the bundler did and how to run the app. The runtime uses the manifest to know what files to serve and how to serve them. - -The real tricky part is making it work the same during DEV and PROD. - -::list -- **Development**: the manifest is generated on the fly, vite-style. -- **Production**: the manifest is generated by the bundler and then saved to disk. The runtime then reads the manifest from disk. -:: - -The manifest API is also the same between the server and client. The only difference is that the client manifest just has access to the router that's serving it. - -You can access the manifest for any router by using `import.meta.env.MANIFEST['']`{lang="ts"}. This will give you a manifest object that looks like this: - -```ts -type Manifest = { - /** Name of the router */ - name: string; - - /** Handler path for the router */ - handler: string; - - inputs: { - [key: string]: { - /** Assets needed by this entry point */ - assets(): Promise; - - output: { - /** Path to built artifact for this entry point. */ - path: string; - }; - }; - }; - - chunks: { - [key: string]: { - assets(): Promise; - output: { - path: string; - }; - }; - }; - - /** - * Seriazable JSON representation of the manifest - * Useful for sending to the client and hydrating the runtime - * by assigning it to `window.manifest` - */ - json(): object; - - /** Map of assets needed by the inputs and chunks */ - assets(): Promise<{ [key: string]: Asset[] }>; -} - -``` diff --git a/docs/content/1.guide/4.cli.md b/docs/content/1.guide/4.cli.md deleted file mode 100644 index e341e004..00000000 --- a/docs/content/1.guide/4.cli.md +++ /dev/null @@ -1,5 +0,0 @@ -# CLI - -## `vinxi dev` - -## `vinxi build` \ No newline at end of file diff --git a/docs/content/1.guide/5.story.md b/docs/content/1.guide/5.story.md deleted file mode 100644 index f6a73709..00000000 --- a/docs/content/1.guide/5.story.md +++ /dev/null @@ -1,469 +0,0 @@ -# Build your server - - -## The beginning - -Create a `app.config.js` file in the root of your project and add the following: - - -```ts [app.config.js] -import { createApp } from "vinxi" - -export default createApp() -``` - -## A static file server - -A simple web server typically serves static files from a directory on disk. Let's add a `public` directory and serve it's contents: - - - -```ts [app.config.js] -import { createApp } from "vinxi" - -export default createApp({ - routers: [ - { - name: "public", - mode: "static", - dir: "./public", - base: "/", - }, - ] -}) -``` - - - -You can already create an `index.html` file in the `public` directory and serve it from the root of your server. - - -```html [public/index.html] - - - - - Hello World - - -

Hello World

- - -``` - -You have yourself a web app! - -Ok, just kidding. There's nothing you can do on the app. Let's add some javascript. - -## JavaScript enters the chat - -We can add a `app.js` file in the `public` directory and add some javascript to it. - - - -```ts [public/app.js] -console.log("Hello World") -``` - - -We need to add the script to the `index.html` file for the browser to actually fetch that code and execute it. - - - -```html {9} [public/index.html] - - - - - Hello World - - -

Hello World

- - - -``` - - - info - -Note how we can use `/app.js` to refer to the `/public/app.js` file from the `index.html` file. This is because we have set the `base` to `/` in the router config. This means that all the files in the `public` directory are served at the routes corresponding to their paths excluding the `public` part, `/public/app.js` is served at `/app.js`. - -If we had set the `base` to `/static`, then the `/public/app.js` file would be served at `/static/app.js`. But so would the `public/index.html`, and that would be a problem. We will deal with that problem later. But its usually a good idea to set the `base` to `/` for the `static` mode. so that people's expectations are met regarding the routes of the files in the `public` directory. - - - -You can now open the browser console and see the message. - - - -But still, there's nothing you can do on the app. Let's add a button. - - - -```html {9} [public/index.html] - - - - - Hello World - - -

Hello World

- - - - -``` - -```ts [public/app.js] -document.getElementById("my-button").addEventListener("click", () => { - console.log("Hello World") -}) -``` - - - -We need some javascript to handle the click of the button. - - - -```ts [public/app.js] -document.getElementById("my-button").addEventListener("click", () => { - console.log("Hello World") -}) -``` - -```html {9} [public/index.html] - - - - - Hello World - - -

Hello World

- - - - -``` - - - -Okay, this is getting fun. Lets add some styles. - - - -```html {6} [public/index.html] - - - - - Hello World - - - -

Hello World

- - - - -``` - -```ts [public/app.js] -document.getElementById("my-button").addEventListener("click", () => { - console.log("Hello World") -}) -``` - - - - - -```css [public/app.css] - -body { - background-color: #000; - color: #fff; -} - -``` - -Ahh, I don't like that background color. Needs more pop. Let's change it to a nice shade of blue. - - - -```css [public/app.css] - -body { - background-color: #0000ff; - color: #fff; -} - -``` - - - -Wait the color on the website didn't change. Well that's annoying. Okay, let's refresh the page. There it is. Okay thats better. - -I don't know about you, but I want some confetti when I click the button. Let's add a library to do that. - -I looked around and found this library called [canvas-confetti](). - -Let's install it. - - - -```bash[npm] -npm install canvas-confetti -``` - -```bash[yarn] -yarn add canvas-confetti -``` - -```bash[pnpm] -pnpm add canvas-confetti -``` - - - -We can now import and use it in our javascript file. - - - -```ts [public/app.js] {1,4} -import confetti from "canvas-confetti" - -document.getElementById("my-button").addEventListener("click", () => { - confetti() -}) -``` - -Let's refresh the page again (I know, I know, it's annoying). Click the button. Erm, nothing happened. Let's check the console. Oh, it says `Cannot use import statement in a script`. We need to tell the browser that we are using ES modules. - - - -```html {6} [public/index.html] - - - - - Hello World - - - -

Hello World

- - - - -``` - - - -Refresh. Click. Confe... Aghh. Nothing again. Let's check the console. Oh, it says `Uncaught TypeError: Failed to resolve module specifier "canvas-confetti". Relative references must start with either "/", "./", or "../".`. Hmm. The browser doesn't know where to find canvas-confetti. The browser knows how to handle URL's. Okay, this is tricky. - -Let's take a look at some of the core problems we have faced: -- We need to refresh the page every time we make a change to the code. This is frustrating and kills momentum -- We want to be able to just install node-modules and use them in our code. But the browser doesn't know how to handle node-modules. - -Before we go down this path, I discovered some other problems faced here: -- I have to declare all my stylesheets in the `index.html` file. Makes it difficult to scope css to components/modules. -- There are some packages that are still in CJS format. The browser doesn't know how to handle that. -- The browser doesn't know about `index.js` or imports without the extension, eg. `import { add } from "./utils"`. -- If I want to write typescript, I need to add a transpile step for the browser to understand it. -- If I want to use React, Vue, etc. I need to add a transpile step for the browser to understand it. - -This is just the tip of the iceberg for thr problems faced with working with bare HTML, CSS and JS. We want to write the code this way. But the browser only understands a certain way of doing things. We need to bridge the gap between the two. This is where Vite comes in. It's a tool that bridges the gap between the way we want to write code and the way the browser understands code. It does this by providing a development server runtime that transforms our code to a format the browser understands. It also provides a builder that transforms our code to a production ready format with a lot of optimizations. It also provides a plugin API that allows us to customize the development server and builder. - -`vinxi` comes with a built-in Vite development server and builder. Let's use it. - -As we have seen before the primitive building block of `vinxi` is a `router`. We have one router that serves static files from the `public` directory. Let's add another router that serves our web app. The entrypoint for our web app is going to be `index.html`. Let's add a router that serves that file. We call this mode where we have one `index.html` file that all routes of the app map to a `spa` mode. - - - -```ts [app.config.js] {12-15} -import { createApp } from "vinxi" - -export default createApp({ - routers: [ - { - name: "public", - mode: "static", - dir: "./public", - base: "/", - }, - { - name: "app", - mode: "spa", - file: "./index.html", - base: "/", - }, - ] -}) -``` - -We will move the files from the `public` directory to the root of the project. Let's run the dev server again. - -All our problems have now been solved. Change something in the CSS. or the JavaScript file. The browser will automatically reload. Install a node-module and use it in your code. The browser will know what to do. Change to typescript, install React, Vue, etc. The browser will know what to do. Because vite tells it. - -Okay now that we all this power, lets thing bigger. What if we sent an email when the button is clicked. We can use the `nodemailer` package to do that. Let's install it. - - - -```bash[npm] -npm install nodemailer -``` - -```bash[yarn] -yarn add nodemailer -``` - -```bash[pnpm] -pnpm add nodemailer -``` - - - -Let's import it and use it in our javascript file. - - - -```ts [app.js] {1,4} -import confetti from "canvas-confetti" -import nodemailer from "nodemailer" - -document.getElementById("my-button").addEventListener("click", () => { - confetti() - nodemailer.sendMail({ - from: " - }) -}) -``` - - -Ohho, the browser is not able to import the nodemailer module. Looks like uses node features that are not available in the browser. Turns out you need to be on the server to send an email this way. Hmm well where do I get a server from. Well, well, well, looks at that. It looks like there was a server there all along. Till now it was using inbuilt handlers to serve static files and the index.html file. But we can add our own handlers to the server. These can be used as API endpoints, or other server functionality. For now, we want to add a handler that sends an email. Let's do that. - -The current routers are in modes that vinxi handles for us. But we can also add routers in `handler` mode. This means that we will handle the request ourselves on the server. Let's add a router in `handler` mode that sends an email. - - - -```ts [app.config.js] {18-24} -import { createApp } from "vinxi" - -export default createApp({ - routers: [ - { - name: "public", - mode: "static", - dir: "./public", - base: "/", - }, - { - name: "app", - mode: "spa", - handler: "./index.html", - base: "/", - }, - { - name: "send-email", - mode: "handler", - handler: "./api/send-email.ts", - base: "/api/send-email", - build: { - target: 'server' - } - } - ] -}) - -``` - - - -We need to create the `api/send-email.ts` file. Let's do that. - - - -```ts [api/send-email.ts] -import nodemailer from "nodemailer" -import { eventHandler } from "vinxi/server" - -export default eventHandler(async (event) => { - await nodemailer.sendMail({ - from: "" - }) - - return 'done' -}) -``` - - - -Now we can call the `/api/send-email` endpoint from our javascript file. - - - -```ts [app.js] {1,4} -import confetti from "canvas-confetti" - -document.getElementById("my-button").addEventListener("click", () => { - confetti() - fetch("/api/send-email", { - method: "POST" - }) -}) -``` - - - -Okay we sending emails now! - -We can even get information from the client and send it in the email. - -Lets send the current time in the email. But not the time on the server, the time on the client when the email request is sent. - - - -```ts [app.js] {7-12} -import confetti from "canvas-confetti" - -document.getElementById("my-button").addEventListener("click", () => { - confetti() - fetch("/api/send-email", { - method: "POST", - body: JSON.stringify({ - time: Date.now() - }), - headers: { - "Content-Type": "application/json" - } - }) -}) -``` - -```ts [api/send-email.ts] {5,8} -import nodemailer from "nodemailer" -import { eventHandler, toWebRequest } from "vinxi/server" - -export default eventHandler(async (event) => { - const { time } = await toWebRequest(event).json() - await nodemailer.sendMail({ - from: "", - text: `Current time: ${time}` - }) - - return 'done' -}) -``` - - - - - - - - - diff --git a/docs/content/1.guide/_dir.yml b/docs/content/1.guide/_dir.yml deleted file mode 100644 index aff9b7cb..00000000 --- a/docs/content/1.guide/_dir.yml +++ /dev/null @@ -1 +0,0 @@ -title: Guide diff --git a/docs/index.html b/docs/index.html index a663bd9c..dd25cf72 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,7 +3,7 @@ - Document + Vinxi diff --git a/packages/vinxi-server-functions/plugin.js b/packages/vinxi-server-functions/plugin.js index c34d2f98..6e6fa94d 100644 --- a/packages/vinxi-server-functions/plugin.js +++ b/packages/vinxi-server-functions/plugin.js @@ -3,16 +3,28 @@ import { fileURLToPath } from "url"; import { client } from "./client.js"; import { server } from "./server.js"; +function createHandlerRouter( + /** @type {import('vinxi').HandlerRouterInput} */ t, +) { + return t; +} + export const serverFunctions = { client: client, server: server, - router: ({ runtime, ...overrides } = {}) => ({ - name: "server-fns", - mode: "handler", - base: "/_server", - handler: fileURLToPath(new URL("./server-handler.js", import.meta.url)), - target: "server", - ...(overrides ?? {}), - plugins: () => [server({ runtime }), ...(overrides?.plugins?.() ?? [])], - }), + router: ( + /** @type {{ runtime?: string } & Partial} */ { + runtime, + ...overrides + } = {}, + ) => + createHandlerRouter({ + name: "server-fns", + mode: "handler", + base: "/_server", + handler: fileURLToPath(new URL("./server-handler.js", import.meta.url)), + target: "server", + ...(overrides ?? {}), + plugins: () => [server({ runtime }), ...(overrides?.plugins?.() ?? [])], + }), }; diff --git a/packages/vinxi/lib/dev-server.js b/packages/vinxi/lib/dev-server.js index ea5ce0dc..9c44cf18 100644 --- a/packages/vinxi/lib/dev-server.js +++ b/packages/vinxi/lib/dev-server.js @@ -112,8 +112,8 @@ export async function createDevServer( app.addRouter(devtoolsClient()); app.addRouter(devtoolsRpc()); } + const { createNitro, writeTypes } = await import("nitropack"); - const { createNitro } = await import("nitropack"); const nitro = await createNitro({ ...app.config.server, rootDir: "", @@ -135,6 +135,7 @@ export async function createDevServer( ...(app.config.server.publicAssets ?? []), ], buildDir: ".vinxi", + imports: false, devHandlers: [ ...( await Promise.all( diff --git a/packages/vinxi/lib/nitro-dev.js b/packages/vinxi/lib/nitro-dev.js index b5ce9b5d..a817dbe5 100644 --- a/packages/vinxi/lib/nitro-dev.js +++ b/packages/vinxi/lib/nitro-dev.js @@ -21,6 +21,7 @@ import { WebSocketServer } from "ws"; import { createServerResponse } from "./http-stream.js"; import { resolve } from "./path.js"; +import { createRouteRulesHandler } from "./route-rules.js"; // import { createVFSHandler } from './vfs' // import defaultErrorHandler from './error' @@ -134,8 +135,19 @@ export function createDevServer(nitro) { // App const app = createApp(); + // Create local fetch callers + const localCall = createCall(promisifyNodeListener(toNodeListener(app))); + const localFetch = createLocalFetch(localCall, globalThis.fetch); + // Debugging endpoint to view vfs // app.use("/_vfs", createVFSHandler(nitro)); + // + app.use( + createRouteRulesHandler({ + localFetch, + routeRules: nitro.options.routeRules, + }), + ); // Serve asset dirs for (const asset of nitro.options.publicAssets) { @@ -258,10 +270,6 @@ export function createDevServer(nitro) { } nitro.hooks.hook("close", close); - // Create local fetch callers - const localCall = createCall(promisifyNodeListener(toNodeListener(app))); - const localFetch = createLocalFetch(localCall, globalThis.fetch); - return { // reload, listen: _listen, diff --git a/packages/vinxi/lib/route-rules.js b/packages/vinxi/lib/route-rules.js new file mode 100644 index 00000000..2f7d4214 --- /dev/null +++ b/packages/vinxi/lib/route-rules.js @@ -0,0 +1,55 @@ +import defu from "defu"; +import { eventHandler, proxyRequest, sendRedirect, setHeaders } from "h3"; +import { createRouter as createRadixRouter, toRouteMatcher } from "radix3"; +import { getQuery, joinURL, withQuery, withoutBase } from "ufo"; + +export function createRouteRulesHandler(ctx) { + const _routeRulesMatcher = toRouteMatcher( + createRadixRouter({ routes: ctx.routeRules }), + ); + + function getRouteRules(event) { + event.context._nitro = event.context._nitro || {}; + if (!event.context._nitro.routeRules) { + event.context._nitro.routeRules = getRouteRulesForPath( + withoutBase(event.path.split("?")[0], "/"), + ); + } + return event.context._nitro.routeRules; + } + function getRouteRulesForPath(path) { + return defu({}, ..._routeRulesMatcher.matchAll(path).reverse()); + } + + return eventHandler((event) => { + const routeRules = getRouteRules(event); + if (routeRules.headers) { + setHeaders(event, routeRules.headers); + } + if (routeRules.redirect) { + return sendRedirect( + event, + routeRules.redirect.to, + routeRules.redirect.statusCode, + ); + } + if (routeRules.proxy) { + let target = routeRules.proxy.to; + if (target.endsWith("/**")) { + let targetPath = event.path; + const strpBase = routeRules.proxy._proxyStripBase; + if (strpBase) { + targetPath = withoutBase(targetPath, strpBase); + } + target = joinURL(target.slice(0, -3), targetPath); + } else if (event.path.includes("?")) { + const query = getQuery(event.path); + target = withQuery(target, query); + } + return proxyRequest(event, target, { + fetch: ctx.localFetch, + ...routeRules.proxy, + }); + } + }); +} diff --git a/packages/vinxi/lib/router-modes.js b/packages/vinxi/lib/router-modes.js index 71b34902..087fbd1b 100644 --- a/packages/vinxi/lib/router-modes.js +++ b/packages/vinxi/lib/router-modes.js @@ -29,7 +29,7 @@ export const buildRouterSchema = z.object({ routes: z.optional(z.custom((value) => value !== null)), extensions: z.array(z.string()).optional(), outDir: z.string().optional(), - target: z.literal("browser").optional().default("browser"), + target: z.enum(["browser"]).default("browser").optional(), plugins: z.optional(z.custom((value) => typeof value === "function")), }); export const handlerRouterSchema = z.object({ @@ -46,7 +46,7 @@ export const handlerRouterSchema = z.object({ /** @type {z.ZodOptionalType>} */ routes: z.optional(z.custom((value) => value !== null)), outDir: z.string().optional(), - target: z.literal("server").optional().default("server"), + target: z.enum(["server"]).default("server").optional(), plugins: z.optional(z.custom((value) => typeof value === "function")), }); export const spaRouterSchema = z.object({ @@ -58,7 +58,7 @@ export const spaRouterSchema = z.object({ routes: z.optional(z.custom((value) => value !== null)), handler: z.string(), outDir: z.string().optional(), - target: z.literal("browser").optional().default("browser"), + target: z.enum(["browser"]).default("browser").optional(), plugins: z.optional(z.custom((value) => typeof value === "function")), }); const customRouterSchema = z.object({ @@ -86,6 +86,7 @@ export const routerSchema = { /** @typedef {z.infer & { outDir: string; base: string; order: number; root: string; internals: Internals }} CustomRouterSchema */ /** @typedef {z.infer & { outDir: string; base: string; order: number; internals: Internals }} StaticRouterSchema */ /** @typedef {z.infer & { outDir: string; base: string; order: number; root: string; internals: Internals }} HandlerRouterSchema */ +/** @typedef {z.infer} HandlerRouterInput */ /** @typedef {z.infer & { outDir: string; base: string; order: number; root: string; internals: Internals }} SPARouterSchema */ /** @typedef {(HandlerRouterSchema | BuildRouterSchema | SPARouterSchema | StaticRouterSchema | CustomRouterSchema )} RouterSchema */ /** @typedef {(z.infer | z.infer | z.infer | z.infer | z.infer)} RouterSchemaInput */ diff --git a/packages/vinxi/package.json b/packages/vinxi/package.json index 2b4424db..4d64c3d6 100644 --- a/packages/vinxi/package.json +++ b/packages/vinxi/package.json @@ -162,6 +162,7 @@ "path-to-regexp": "^6.2.1", "pathe": "^1.1.1", "perfect-debounce": "^1.0.0", + "radix3": "^1.1.0", "resolve": "^1.22.6", "rollup-plugin-visualizer": "^5.9.2", "serve-placeholder": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f1cb072..0b93913f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -948,6 +948,9 @@ importers: perfect-debounce: specifier: ^1.0.0 version: 1.0.0 + radix3: + specifier: ^1.1.0 + version: 1.1.0 resolve: specifier: ^1.22.6 version: 1.22.8 @@ -968,7 +971,7 @@ importers: version: 1.7.4 unimport: specifier: ^3.2.0 - version: 3.4.0(rollup@3.29.4) + version: 3.4.0 unstorage: specifier: ^1.9.0 version: 1.9.0 @@ -1008,7 +1011,7 @@ importers: version: 1.8.5 vite-plugin-inspect: specifier: ^0.7.38 - version: 0.7.41(vite@4.5.0) + version: 0.7.41(rollup@4.2.0)(vite@4.5.0) vite-plugin-solid: specifier: ^2.7.0 version: 2.7.2(solid-js@1.8.5)(vite@4.5.0) @@ -4994,6 +4997,7 @@ packages: estree-walker: 2.0.2 picomatch: 2.3.1 rollup: 3.29.4 + dev: false /@rollup/pluginutils@5.0.5(rollup@4.2.0): resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} @@ -5008,14 +5012,12 @@ packages: estree-walker: 2.0.2 picomatch: 2.3.1 rollup: 4.2.0 - dev: false /@rollup/rollup-android-arm-eabi@4.2.0: resolution: {integrity: sha512-8PlggAxGxavr+pkCNeV1TM2wTb2o+cUWDg9M1cm9nR27Dsn287uZtSLYXoQqQcmq+sYfF7lHfd3sWJJinH9GmA==} cpu: [arm] os: [android] requiresBuild: true - dev: false optional: true /@rollup/rollup-android-arm64@4.2.0: @@ -5023,7 +5025,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: false optional: true /@rollup/rollup-darwin-arm64@4.2.0: @@ -5031,7 +5032,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: false optional: true /@rollup/rollup-darwin-x64@4.2.0: @@ -5039,7 +5039,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: false optional: true /@rollup/rollup-linux-arm-gnueabihf@4.2.0: @@ -5047,7 +5046,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: false optional: true /@rollup/rollup-linux-arm64-gnu@4.2.0: @@ -5055,7 +5053,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: false optional: true /@rollup/rollup-linux-arm64-musl@4.2.0: @@ -5063,7 +5060,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: false optional: true /@rollup/rollup-linux-x64-gnu@4.2.0: @@ -5071,7 +5067,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: false optional: true /@rollup/rollup-linux-x64-musl@4.2.0: @@ -5079,7 +5074,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: false optional: true /@rollup/rollup-win32-arm64-msvc@4.2.0: @@ -5087,7 +5081,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: false optional: true /@rollup/rollup-win32-ia32-msvc@4.2.0: @@ -5095,7 +5088,6 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: false optional: true /@rollup/rollup-win32-x64-msvc@4.2.0: @@ -5103,7 +5095,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: false optional: true /@sideway/address@4.1.4: @@ -5689,7 +5680,7 @@ packages: hasBin: true dependencies: '@ampproject/remapping': 2.2.1 - '@rollup/pluginutils': 5.0.5(rollup@3.29.4) + '@rollup/pluginutils': 5.0.5(rollup@4.2.0) '@unocss/config': 0.56.5 '@unocss/core': 0.56.5 '@unocss/preset-uno': 0.56.5 @@ -5864,7 +5855,7 @@ packages: vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 dependencies: '@ampproject/remapping': 2.2.1 - '@rollup/pluginutils': 5.0.5(rollup@3.29.4) + '@rollup/pluginutils': 5.0.5(rollup@4.2.0) '@unocss/config': 0.56.5 '@unocss/core': 0.56.5 '@unocss/inspector': 0.56.5 @@ -10908,7 +10899,6 @@ packages: '@rollup/rollup-win32-ia32-msvc': 4.2.0 '@rollup/rollup-win32-x64-msvc': 4.2.0 fsevents: 2.3.3 - dev: false /run-applescript@5.0.0: resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} @@ -11824,6 +11814,24 @@ packages: vfile: 4.2.1 dev: false + /unimport@3.4.0: + resolution: {integrity: sha512-M/lfFEgufIT156QAr/jWHLUn55kEmxBBiQsMxvRSIbquwmeJEyQYgshHDEvQDWlSJrVOOTAgnJ3FvlsrpGkanA==} + dependencies: + '@rollup/pluginutils': 5.0.5(rollup@4.2.0) + escape-string-regexp: 5.0.0 + fast-glob: 3.3.1 + local-pkg: 0.4.3 + magic-string: 0.30.5 + mlly: 1.4.2 + pathe: 1.1.1 + pkg-types: 1.0.3 + scule: 1.0.0 + strip-literal: 1.3.0 + unplugin: 1.5.0 + transitivePeerDependencies: + - rollup + dev: false + /unimport@3.4.0(rollup@3.29.4): resolution: {integrity: sha512-M/lfFEgufIT156QAr/jWHLUn55kEmxBBiQsMxvRSIbquwmeJEyQYgshHDEvQDWlSJrVOOTAgnJ3FvlsrpGkanA==} dependencies: @@ -12239,7 +12247,7 @@ packages: serve-static: 1.15.0 ufo: 1.3.1 unenv: 1.7.4 - unimport: 3.4.0(rollup@3.29.4) + unimport: 3.4.0 unstorage: 1.9.0 vite: 4.3.9(@types/node@18.18.8) zod: 3.22.4 @@ -12314,30 +12322,6 @@ packages: - supports-color dev: false - /vite-plugin-inspect@0.7.41(vite@4.5.0): - resolution: {integrity: sha512-gASdFRO4CHDQF8qAk9LZEJyzlIJM4bFvDn7hz0e2r1PS6uq+yukd8+jHctOAbvCceQoTS5iDAgd4/mWcGWYoMw==} - engines: {node: '>=14'} - peerDependencies: - '@nuxt/kit': '*' - vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 - peerDependenciesMeta: - '@nuxt/kit': - optional: true - dependencies: - '@antfu/utils': 0.7.6 - '@rollup/pluginutils': 5.0.5(rollup@3.29.4) - debug: 4.3.4 - error-stack-parser-es: 0.1.1 - fs-extra: 11.1.1 - open: 9.1.0 - picocolors: 1.0.0 - sirv: 2.0.3 - vite: 4.5.0(@types/node@14.18.63) - transitivePeerDependencies: - - rollup - - supports-color - dev: false - /vite-plugin-solid@2.7.2(solid-js@1.8.5)(vite@4.5.0): resolution: {integrity: sha512-GV2SMLAibOoXe76i02AsjAg7sbm/0lngBlERvJKVN67HOrJsHcWgkt0R6sfGLDJuFkv2aBe14Zm4vJcNME+7zw==} peerDependencies: