├── .gitignore ├── public └── hat.jpg ├── src ├── pages │ ├── about.tsx │ └── index.tsx ├── counter.tsx ├── get-posts.ts ├── api.ts ├── post-list.tsx └── index.tsx ├── index.html ├── package.json └── app.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /public/hat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnwithjason/build-your-own-metaframework/main/public/hat.jpg -------------------------------------------------------------------------------- /src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => { 4 | return

About

; 5 | }; 6 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => { 4 | return

Home

; 5 | }; 6 | -------------------------------------------------------------------------------- /src/counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export const Counter = () => { 4 | const [count, setCount] = useState(0); 5 | 6 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /src/get-posts.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | export async function getPosts() { 4 | const res = await fetch('https://www.learnwithjason.dev/api/v2/episodes'); 5 | 6 | if (!res.ok) { 7 | return { error: true }; 8 | } 9 | 10 | const data = await res.json(); 11 | 12 | console.log('server!'); 13 | 14 | return data; 15 | } 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Own Metaframework 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import { eventHandler } from 'vinxi/http'; 2 | 3 | export default eventHandler(async (event) => { 4 | const method = event.method; 5 | const path = event.path; 6 | 7 | if (method === 'GET' && path === '/posts') { 8 | const res = await fetch('https://www.learnwithjason.dev/api/v2/episodes'); 9 | 10 | if (!res.ok) { 11 | return { error: true }; 12 | } 13 | 14 | const data = await res.json(); 15 | 16 | return data; 17 | } 18 | 19 | return { test: 'on' }; 20 | }); 21 | -------------------------------------------------------------------------------- /src/post-list.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { getPosts } from './get-posts'; 3 | 4 | export const PostList = () => { 5 | const [posts, setPosts] = useState([]); 6 | 7 | useEffect(() => { 8 | async function loadPosts() { 9 | const data = await getPosts(); 10 | 11 | setPosts(data); 12 | } 13 | 14 | loadPosts(); 15 | }, []); 16 | 17 | return ( 18 | <> 19 |

Posts

20 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "build-your-own-metaframework", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "vinxi dev", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "dependencies": { 15 | "@types/react": "^18.3.3", 16 | "@types/react-dom": "^18.3.0", 17 | "@vinxi/react": "^0.2.3", 18 | "@vinxi/server-functions": "^0.3.3", 19 | "@vitejs/plugin-react": "^4.3.1", 20 | "react": "^18.3.1", 21 | "react-dom": "^18.3.1", 22 | "vinxi": "^0.3.14", 23 | "wouter": "^3.3.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { Route, Link, Router } from 'wouter'; 4 | import { lazyRoute } from '@vinxi/react'; 5 | import { getManifest } from 'vinxi/manifest'; 6 | import fileRoutes from 'vinxi/routes'; 7 | import { pathToRegexp } from 'path-to-regexp'; 8 | import { Counter } from './counter'; 9 | import { PostList } from './post-list'; 10 | 11 | const routes = fileRoutes.map((route) => { 12 | return { 13 | ...route, 14 | component: lazyRoute( 15 | route.$component, 16 | getManifest('client'), 17 | getManifest('client'), 18 | ), 19 | }; 20 | }); 21 | 22 | const convertPathToRegexp = (path: string) => { 23 | let keys = []; 24 | const pattern = pathToRegexp(path, keys, { strict: true }); 25 | return { keys, pattern }; 26 | }; 27 | 28 | createRoot(document.getElementById('root')!).render( 29 | 30 | 31 | hat 32 | 33 | 38 | 39 | {routes.map((route) => ( 40 | 41 | ))} 42 | 43 | , 44 | ); 45 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | import { createApp, resolve } from 'vinxi'; 2 | import { BaseFileSystemRouter, cleanPath } from 'vinxi/fs-router'; 3 | import reactPlugin from '@vitejs/plugin-react'; 4 | import { serverFunctions } from '@vinxi/server-functions/plugin'; 5 | 6 | class FileRouter extends BaseFileSystemRouter { 7 | toPath(src: any): string { 8 | const path = cleanPath(src, this.config) 9 | .slice(1) 10 | .replace('index', '') 11 | .replace(/\[([^\/]+)\]/g, (_, m) => { 12 | if (m.length > 3 && m.startsWith('...')) { 13 | return `*${m.slice(3)}`; 14 | } 15 | return `:${m}`; 16 | }); 17 | 18 | return `/${path}`; 19 | } 20 | 21 | toRoute(src: any) { 22 | const path = this.toPath(src); 23 | 24 | return { 25 | $component: { 26 | src, 27 | pick: ['default'], 28 | }, 29 | path, 30 | filePath: src, 31 | }; 32 | } 33 | } 34 | 35 | export default createApp({ 36 | routers: [ 37 | { 38 | type: 'spa', 39 | name: 'client', 40 | handler: './index.html', 41 | plugins: () => [reactPlugin(), serverFunctions.client()], 42 | routes: (router, app) => { 43 | return new FileRouter( 44 | { 45 | dir: resolve.absolute('./src/pages', router.root!), 46 | extensions: ['tsx', 'jsx', 'ts', 'js'], 47 | }, 48 | router, 49 | app, 50 | ); 51 | }, 52 | }, 53 | { 54 | type: 'http', 55 | name: 'api', 56 | handler: './src/api.ts', 57 | base: '/api', 58 | }, 59 | { 60 | type: 'static', 61 | name: 'static', 62 | dir: './public', 63 | }, 64 | serverFunctions.router(), 65 | ], 66 | }); 67 | --------------------------------------------------------------------------------