├── .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 |
21 | {posts.map((post) => (
22 | - {post.title}
23 | ))}
24 |
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 |
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 |
--------------------------------------------------------------------------------