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 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,13 @@
1
+ {
2
+ "name": "create-youtiao",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-youtiao": "./index.ts"
8
+ },
9
+ "files": [
10
+ "index.ts",
11
+ "template"
12
+ ]
13
+ }
@@ -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,6 @@
1
+ <h1>About</h1>
2
+ <p>Built with <a href="https://github.com/zenazn/youtiao">Youtiao</a> — a Svelte 5 + Bun web framework.</p>
3
+
4
+ <nav>
5
+ <a href="/">Home</a>
6
+ </nav>
@@ -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,5 @@
1
+ export default {
2
+ compilerOptions: {
3
+ experimental: { async: true },
4
+ },
5
+ };
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ // import type { YoutiaConfig } from "youtiao";
2
+
3
+ // export default {
4
+ // routes: "src/routes",
5
+ // output: "dist",
6
+ // rpc: "src/server.ts",
7
+ // binaryName: "app",
8
+ // server: {
9
+ // port: 3000,
10
+ // },
11
+ // } satisfies YoutiaConfig;