create-youtiao 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +44 -0
- package/package.json +13 -0
- package/template/CLAUDE.md +116 -0
- package/template/README.md +140 -0
- package/template/package.json +15 -0
- package/template/src/routes/+index.svelte +16 -0
- package/template/src/routes/about.svelte +6 -0
- package/template/src/server.ts +18 -0
- package/template/svelte.config.js +5 -0
- package/template/tsconfig.json +15 -0
- package/template/youtiao.config.ts +11 -0
package/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { resolve, basename } from "node:path";
|
|
4
|
+
import { cp, readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
|
|
6
|
+
const projectName = process.argv[2];
|
|
7
|
+
|
|
8
|
+
if (!projectName) {
|
|
9
|
+
console.log(`Usage: bunx create-youtiao <project-name>`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const dest = resolve(process.cwd(), projectName);
|
|
14
|
+
const templateDir = resolve(import.meta.dir, "template");
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await stat(dest);
|
|
18
|
+
console.error(`Destination already exists: ${dest}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
const errno = err as NodeJS.ErrnoException;
|
|
22
|
+
if (errno.code !== "ENOENT") {
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`Creating ${projectName}...`);
|
|
28
|
+
|
|
29
|
+
// Copy template
|
|
30
|
+
await cp(templateDir, dest, { recursive: true });
|
|
31
|
+
|
|
32
|
+
// Update package.json name
|
|
33
|
+
const pkgPath = resolve(dest, "package.json");
|
|
34
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
35
|
+
pkg.name = basename(projectName);
|
|
36
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
37
|
+
|
|
38
|
+
console.log(`
|
|
39
|
+
Done! To get started:
|
|
40
|
+
|
|
41
|
+
cd ${projectName}
|
|
42
|
+
bun install
|
|
43
|
+
bunx youtiao dev
|
|
44
|
+
`);
|
package/package.json
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
## Stack
|
|
2
|
+
|
|
3
|
+
- **Bun** — Runtime, bundler, test runner, package manager
|
|
4
|
+
- **Youtiao** — Svelte 5 + Bun web framework with filesystem routing, SSR, and hydration
|
|
5
|
+
- **GraphPC** — RPC framework (like tRPC with a graph). Docs: `node_modules/graphpc/docs/llm.md`
|
|
6
|
+
- **Svelte 5** — Uses runes (`$props`, `$state`, `$derived`, etc.)
|
|
7
|
+
- **Zod** — Schema validation
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
- `bun install` — Install dependencies
|
|
12
|
+
- `bunx youtiao dev` — Start the dev server
|
|
13
|
+
- `bunx youtiao build` — Create a production build
|
|
14
|
+
- `bun test` — Run tests
|
|
15
|
+
- `bunx tsc` — Type-check
|
|
16
|
+
|
|
17
|
+
## Project Layout
|
|
18
|
+
|
|
19
|
+
- `src/routes/` — Filesystem-routed Svelte pages
|
|
20
|
+
- `src/server.ts` — GraphPC API definition, auto-loaded by the framework for SSR + WebSocket RPC
|
|
21
|
+
- `youtiao.config.ts` — Framework config (routes dir, output dir, server port, etc.)
|
|
22
|
+
|
|
23
|
+
## Routing
|
|
24
|
+
|
|
25
|
+
Pages are `.svelte` files in `src/routes/`. Special files:
|
|
26
|
+
- `+index.svelte` — Directory index page
|
|
27
|
+
- `+layout.svelte` — Layout wrapper (nests with parent layouts)
|
|
28
|
+
- `+error.svelte` — Error boundary
|
|
29
|
+
- `[param].svelte` — Dynamic route segment
|
|
30
|
+
- `[*].svelte` — Catch-all route
|
|
31
|
+
|
|
32
|
+
Pages receive `rpc` and `params` as props.
|
|
33
|
+
|
|
34
|
+
## RPC
|
|
35
|
+
|
|
36
|
+
`src/server.ts` exports a GraphPC server. Pages access it via the `rpc` prop — calls run directly during SSR, over WebSocket on the client. Read `node_modules/graphpc/docs/llm.md` for the full GraphPC API.
|
|
37
|
+
|
|
38
|
+
## Svelte + GraphPC Patterns
|
|
39
|
+
|
|
40
|
+
Your API is an object graph. Navigate it with dot notation, `await` to read, call methods to act. It feels like working with local objects — the network is invisible.
|
|
41
|
+
|
|
42
|
+
### Navigate and read
|
|
43
|
+
Edges (`@edge`) traverse the graph synchronously — no network call, just returns a stub. `await stub` fetches the node's data (public properties and getters). Wrap in `$derived` for reactivity, or use plain `await` for data that won't change during the component's lifetime.
|
|
44
|
+
|
|
45
|
+
```svelte
|
|
46
|
+
<script>
|
|
47
|
+
const post = rpc.posts.get(Number(params.id)); // edge traversal, sync
|
|
48
|
+
const data = $derived(await $post); // reactive read — re-runs when post updates
|
|
49
|
+
const author = await post.author; // one-time read — fine for static data
|
|
50
|
+
</script>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Mutate
|
|
54
|
+
Methods (`@method`) are actions — always async, always hit the server. When a method returns `ref()`, the cache updates automatically and any `$derived` watching that node re-runs. No manual refresh needed.
|
|
55
|
+
|
|
56
|
+
```svelte
|
|
57
|
+
<script>
|
|
58
|
+
async function handleLike() {
|
|
59
|
+
await post.like();
|
|
60
|
+
// like() returns ref(Post, id) → cache updates → $derived(await $post) re-fires
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For structural changes (adding/removing items from a collection), `invalidate()` the container so it re-fetches its list:
|
|
66
|
+
|
|
67
|
+
```svelte
|
|
68
|
+
<script>
|
|
69
|
+
async function deletePost(id) {
|
|
70
|
+
await rpc.posts.remove(id);
|
|
71
|
+
invalidate(page);
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Compose with subtrees
|
|
77
|
+
Pass components the narrowest slice of the graph they need. Edge traversal within the component produces observable stubs automatically — each component manages its own data.
|
|
78
|
+
|
|
79
|
+
```svelte
|
|
80
|
+
<PostCard post={rpc.posts.get(id)} /> <!-- PostCard does $derived(await $post) -->
|
|
81
|
+
<CommentThread comments={post.comments} /> <!-- manages its own CRUD internally -->
|
|
82
|
+
<StatsSidebar stats={rpc.stats} /> <!-- reads aggregates, consumes streams -->
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Collections
|
|
86
|
+
Model collections as page nodes entered via `@edge`. The page's items are a data field — one `await` gives you everything. See `node_modules/graphpc/docs/patterns.md` for pagination.
|
|
87
|
+
|
|
88
|
+
```svelte
|
|
89
|
+
<script>
|
|
90
|
+
const page = rpc.posts.page;
|
|
91
|
+
const items = $derived(await $page.items());
|
|
92
|
+
</script>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Streams
|
|
96
|
+
`@stream` async generators push live data to the client. They're inert during SSR — consume them in `$effect` with a cleanup function.
|
|
97
|
+
|
|
98
|
+
```svelte
|
|
99
|
+
<script>
|
|
100
|
+
let events = $state([]);
|
|
101
|
+
$effect(() => {
|
|
102
|
+
const stream = stats.liveActivity();
|
|
103
|
+
let active = true;
|
|
104
|
+
(async () => {
|
|
105
|
+
for await (const event of stream) {
|
|
106
|
+
if (!active) break;
|
|
107
|
+
events = [event, ...events].slice(0, 20);
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
return () => { active = false; };
|
|
111
|
+
});
|
|
112
|
+
</script>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Pitfall: invalidation loops
|
|
116
|
+
Avoid `$derived(await $stub.method())` when the method returns refs that are descendants of the subscribed path — this creates an infinite loop. The page edge pattern sidesteps this by design.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# my-youtiao-app
|
|
2
|
+
|
|
3
|
+
Built with [Youtiao](https://github.com/zenazn/youtiao) — a Svelte 5 + Bun web framework.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
bunx youtiao dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open http://localhost:3000. The dev server recompiles on every request.
|
|
13
|
+
|
|
14
|
+
## Project Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/
|
|
18
|
+
routes/
|
|
19
|
+
+index.svelte # Home page (/)
|
|
20
|
+
about.svelte # /about
|
|
21
|
+
+layout.svelte # Root layout (optional)
|
|
22
|
+
+error.svelte # Error boundary (optional)
|
|
23
|
+
server.ts # GraphPC API
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Pages
|
|
27
|
+
|
|
28
|
+
Pages are `.svelte` files in `src/routes/`. Every page receives `rpc` and `params` as props:
|
|
29
|
+
|
|
30
|
+
```svelte
|
|
31
|
+
<script>
|
|
32
|
+
let { rpc, params } = $props();
|
|
33
|
+
</script>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Routing
|
|
37
|
+
|
|
38
|
+
| File | Route |
|
|
39
|
+
|------|-------|
|
|
40
|
+
| `+index.svelte` | Directory index |
|
|
41
|
+
| `about.svelte` | `/about` |
|
|
42
|
+
| `posts/[id].svelte` | `/posts/:id` (dynamic) |
|
|
43
|
+
| `files/[*].svelte` | `/files/*` (catch-all) |
|
|
44
|
+
| `+layout.svelte` | Wraps all pages in that directory (nests) |
|
|
45
|
+
| `+error.svelte` | Catches errors from child pages |
|
|
46
|
+
|
|
47
|
+
### Layouts
|
|
48
|
+
|
|
49
|
+
A `+layout.svelte` wraps all pages in its directory and below. Layouts nest.
|
|
50
|
+
|
|
51
|
+
```svelte
|
|
52
|
+
<script>
|
|
53
|
+
let { children } = $props();
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<nav>...</nav>
|
|
57
|
+
<main>{@render children()}</main>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## RPC
|
|
61
|
+
|
|
62
|
+
Your API lives in `src/server.ts`, built with [GraphPC](https://github.com/zenazn/graphpc) — a typed RPC library where the API is a navigable object graph. Define classes extending `Node`, decorate methods and edges, export `createRpcRoot(ctx)` for SSR, and optionally export `getRequestContext(req)` when you need auth or request-scoped context.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { Node, edge, method, createServer } from "graphpc";
|
|
66
|
+
import { z } from "zod";
|
|
67
|
+
|
|
68
|
+
class Post extends Node {
|
|
69
|
+
id: string;
|
|
70
|
+
title: string;
|
|
71
|
+
|
|
72
|
+
constructor(id: string) {
|
|
73
|
+
super();
|
|
74
|
+
Object.assign(this, db.posts.get(id));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@method(z.string())
|
|
78
|
+
async updateTitle(title: string) {
|
|
79
|
+
await db.posts.update(this.id, { title });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class Posts extends Node {
|
|
84
|
+
@edge(Post, z.string())
|
|
85
|
+
get(id: string) { return new Post(id); }
|
|
86
|
+
|
|
87
|
+
@method
|
|
88
|
+
async count() { return db.posts.count(); }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class Api extends Node {
|
|
92
|
+
@edge(Posts)
|
|
93
|
+
get posts() { return new Posts(); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getRequestContext(_req: Request) {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createRpcRoot(_ctx: {}) {
|
|
101
|
+
return new Api();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const server = createServer({}, createRpcRoot);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Then use it in any page — edge navigation is synchronous, data access and methods are awaited:
|
|
108
|
+
|
|
109
|
+
```svelte
|
|
110
|
+
<script>
|
|
111
|
+
let { rpc } = $props();
|
|
112
|
+
const post = rpc.posts.get("1"); // sync — no network call
|
|
113
|
+
const { id, title } = await post; // fetches data
|
|
114
|
+
// await post.updateTitle("New"); // calls method
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<h1>{title}</h1>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
During SSR, RPC calls execute against `createRpcRoot(ctx)`. For WebSocket connections, `getRequestContext(req)` derives context from the upgrade request, which is then passed to `createRpcRoot(ctx)`. On the client, RPC calls use a WebSocket to `/rpc`, and hydration data is embedded in the HTML so the client doesn't re-fetch on load.
|
|
121
|
+
|
|
122
|
+
### Validation
|
|
123
|
+
|
|
124
|
+
GraphPC uses [Standard Schema](https://standardschema.dev/) for parameter validation — pass schemas to `@method()` and `@edge()`. [Zod](https://zod.dev/) is included, but any Standard Schema-compatible library works (valibot, arktype, etc.).
|
|
125
|
+
|
|
126
|
+
## Production Build
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
bunx youtiao build
|
|
130
|
+
./dist/app
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Compiles all routes, generates optimized client assets, and produces a single binary.
|
|
134
|
+
|
|
135
|
+
## Learn More
|
|
136
|
+
|
|
137
|
+
- [Youtiao docs](https://github.com/zenazn/youtiao#readme)
|
|
138
|
+
- [GraphPC docs](https://github.com/zenazn/graphpc#readme)
|
|
139
|
+
- [Svelte 5 docs](https://svelte.dev/docs)
|
|
140
|
+
- [Bun docs](https://bun.sh/docs)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-youtiao-app",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"youtiao": "^0.1.0",
|
|
7
|
+
"graphpc": "^0.9.3",
|
|
8
|
+
"svelte": "^5.53.13",
|
|
9
|
+
"zod": "^4.3.6"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/bun": "latest",
|
|
13
|
+
"typescript": "^5"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let { rpc } = $props();
|
|
3
|
+
const greeting = await rpc.hello("world");
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<h1>Welcome to Youtiao</h1>
|
|
7
|
+
<p>{greeting}</p>
|
|
8
|
+
|
|
9
|
+
<nav>
|
|
10
|
+
<a href="/about">About</a>
|
|
11
|
+
</nav>
|
|
12
|
+
|
|
13
|
+
<style>
|
|
14
|
+
h1 { color: #e65100; }
|
|
15
|
+
nav { margin-top: 1rem; }
|
|
16
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createServer, Node, method } from "graphpc";
|
|
2
|
+
|
|
3
|
+
export class Api extends Node {
|
|
4
|
+
@method
|
|
5
|
+
async hello(name: string): Promise<string> {
|
|
6
|
+
return `Hello, ${name}!`;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getRequestContext(_req: Request) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createRpcRoot(_ctx: {}) {
|
|
15
|
+
return new Api();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const server = createServer({}, createRpcRoot);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "Preserve",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"verbatimModuleSyntax": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
}
|
|
15
|
+
}
|