Skip to content

Commit 451d3f8

Browse files
committed
feat(docs): TS build setup guide + auto-import DX across all pages
Add dedicated TypeScript Build Setup guide covering aster-gen CLI, npm scripts, Vite/Webpack plugins, type mapping, branded numerics, and runtime fallback. All TS code examples simplified: bare decorators (@rpc(), @ServerStream(), etc.), no manual registerGenerated() import, no generated wiring — just `npx aster-gen` then AsterServer.start() auto-imports aster-rpc.generated.js. - New page: guides/typescript-build-setup.mdx (added to sidebar) - quickstart/python.mdx: TS producer uses auto-import, node not bun - quickstart/mission-control.mdx: all 5 chapter TS blocks cleaned up, Bun.spawn replaced with node:child_process for cross-runtime compat - bindings/javascript/index.mdx: bare @rpc() + auto-import example - bindings/javascript/server.mdx: decorator docs stripped of request/response, link to build setup - guides/define-a-service.mdx: build step note after TS tab
1 parent 1401e10 commit 451d3f8

7 files changed

Lines changed: 230 additions & 34 deletions

File tree

docs/bindings/javascript/index.mdx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ A producer hosts a service over QUIC; a consumer connects by address.
3939

4040
```typescript
4141
// service.ts
42-
import { Service, Rpc, AsterServer, AsterClientWrapper } from '@aster-rpc/aster';
42+
import { Service, Rpc, AsterServer } from '@aster-rpc/aster';
4343

4444
class HelloRequest {
4545
name = "";
@@ -53,14 +53,16 @@ class HelloResponse {
5353

5454
@Service({ name: "Hello", version: 1 })
5555
export class HelloService {
56-
@Rpc({ request: HelloRequest, response: HelloResponse })
56+
@Rpc()
5757
async sayHello(req: HelloRequest): Promise<HelloResponse> {
5858
return new HelloResponse({ message: `Hello, ${req.name}!` });
5959
}
6060
}
6161

62-
// Producer
63-
const server = new AsterServer({ services: [new HelloService()] });
62+
// Producer (run `npx aster-gen` first)
63+
const server = new AsterServer({
64+
services: [new HelloService()],
65+
});
6466
await server.start();
6567
console.log("Producer ready at:", server.address);
6668
await server.serve();

docs/bindings/javascript/server.mdx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,36 +89,30 @@ class EchoService { ... }
8989
### @Rpc
9090

9191
```typescript
92-
@Rpc({ request: EchoRequest, response: EchoResponse, timeout: 30, idempotent: true })
92+
@Rpc({ timeout: 30, idempotent: true })
9393
async echo(req: EchoRequest): Promise<EchoResponse> { ... }
9494
```
9595

96-
> **Note:** TypeScript erases generic type parameters at runtime, so the
97-
> contract publisher and the `gen-client` codegen pipeline cannot discover
98-
> the request and response shapes from the method signature alone. Pass
99-
> the constructors explicitly via `request:` and `response:` so the
100-
> published manifest carries the wire tag and field list -- this is
101-
> required for cross-language consumers (Python, Go, Java) to talk to
102-
> the service.
96+
The `aster-gen` build step reads request/response types from the AST, so you don't need to pass them in the decorator. Optional keys: `timeout`, `idempotent`, `requires`, `serialization`. See [TypeScript Build Setup](/docs/guides/typescript-build-setup).
10397

10498
### @ServerStream
10599

106100
```typescript
107-
@ServerStream({ request: WatchRequest, response: ItemUpdate })
101+
@ServerStream()
108102
async *watchItems(req: WatchRequest): AsyncGenerator<ItemUpdate> { ... }
109103
```
110104

111105
### @ClientStream
112106

113107
```typescript
114-
@ClientStream({ request: Item, response: BatchResult })
108+
@ClientStream()
115109
async uploadBatch(requests: AsyncIterable<Item>): Promise<BatchResult> { ... }
116110
```
117111

118112
### @BidiStream
119113

120114
```typescript
121-
@BidiStream({ request: Message, response: Message })
115+
@BidiStream()
122116
async *chat(requests: AsyncIterable<Message>): AsyncGenerator<Message> { ... }
123117
```
124118

docs/guides/define-a-service.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ class TaskService {
163163
}
164164
```
165165

166-
`@Rpc()` accepts optional `{ timeout, idempotent, requires }` options.
166+
`@Rpc()` accepts optional `{ timeout, idempotent, requires }` options. The scanner reads request/response types from the AST — run `npx aster-gen` before starting your server. See [TypeScript Build Setup](/docs/guides/typescript-build-setup).
167167

168168
</TabItem>
169169
</LanguageTabs>
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
---
2+
id: typescript-build-setup
3+
title: TypeScript Build Setup
4+
slug: /guides/typescript-build-setup
5+
---
6+
7+
TypeScript erases type information at runtime. Aster's `aster-gen` scanner
8+
reads your source at build time via the TypeScript compiler API, discovers
9+
all `@Service`, `@WireType`, `@Rpc`, `@ServerStream`, `@ClientStream`, and
10+
`@BidiStream` decorated classes, and emits `aster-rpc.generated.ts` with
11+
the metadata the runtime needs — field shapes, wire type mappings, method
12+
signatures, and contract identity hashes.
13+
14+
This replaces the need to pass `{ request, response }` to every decorator.
15+
You write clean decorators; the scanner extracts types from the AST.
16+
17+
## Install
18+
19+
```bash
20+
npm install @aster-rpc/aster
21+
```
22+
23+
`aster-gen` ships as the `aster-gen` bin inside `@aster-rpc/aster` — no
24+
extra package needed. TypeScript is a peer dependency (bring your own
25+
version).
26+
27+
## Run the scanner
28+
29+
```bash
30+
# defaults: reads ./tsconfig.json, writes ./aster-rpc.generated.ts
31+
npx aster-gen
32+
33+
# custom paths
34+
npx aster-gen -p tsconfig.app.json -o build/aster-rpc.generated.ts
35+
```
36+
37+
Or add it to your `package.json`:
38+
39+
```json
40+
{
41+
"scripts": {
42+
"build": "aster-gen && tsc"
43+
}
44+
}
45+
```
46+
47+
Re-run after adding or changing wire types, RPC methods, or decorator
48+
options. The generated file is deterministic — same input always produces
49+
the same output.
50+
51+
## Use in your app
52+
53+
`AsterServer.start()` auto-imports `aster-rpc.generated.js` from the
54+
working directory — no manual import or wiring needed:
55+
56+
```typescript
57+
import { AsterServer } from '@aster-rpc/aster';
58+
import { MyService } from './services.js';
59+
60+
const server = new AsterServer({
61+
services: [new MyService()],
62+
});
63+
await server.start();
64+
await server.serve();
65+
```
66+
67+
If the generated file is missing, the runtime falls back to reflection
68+
and logs a warning.
69+
70+
## What gets generated
71+
72+
The output `aster-rpc.generated.ts` contains two exports:
73+
74+
- **`WIRE_TYPES`** — ordered array of wire type descriptors (tag, fields,
75+
nested type refs, field name sets) in dependency order.
76+
- **`SERVICES`** — array of service descriptors with method signatures,
77+
RPC patterns, pre-derived manifest fields, and BLAKE3 type hashes for
78+
cross-language contract identity.
79+
80+
The file is `// @ts-nocheck` and auto-formatted. Commit it or gitignore
81+
it — either works. Committing makes the example runnable without a build
82+
step; gitignoring keeps diffs clean.
83+
84+
## Bundler plugins
85+
86+
For projects using a bundler, plugins run `aster-gen` automatically on
87+
build and during hot reload. When using a bundler plugin, pass the
88+
`generated` option explicitly since dynamic import won't resolve at
89+
bundle time:
90+
91+
### Vite
92+
93+
```typescript
94+
// vite.config.ts
95+
import { defineConfig } from 'vite';
96+
import { asterGen } from '@aster-rpc/aster/vite-plugin';
97+
98+
export default defineConfig({
99+
plugins: [
100+
asterGen({
101+
project: 'tsconfig.json',
102+
out: 'aster-rpc.generated.ts',
103+
}),
104+
],
105+
});
106+
```
107+
108+
The plugin runs on `buildStart` and watches for changes to decorated
109+
source files during dev.
110+
111+
### Webpack
112+
113+
```typescript
114+
// webpack.config.ts
115+
import { AsterGenPlugin } from '@aster-rpc/aster/webpack-plugin';
116+
117+
export default {
118+
plugins: [
119+
new AsterGenPlugin({
120+
project: 'tsconfig.json',
121+
out: 'aster-rpc.generated.ts',
122+
}),
123+
],
124+
};
125+
```
126+
127+
The plugin hooks into `beforeCompile` and re-scans when source files
128+
change.
129+
130+
## Type mapping
131+
132+
The scanner maps TypeScript types to wire types per the Aster spec:
133+
134+
| TypeScript | Wire type |
135+
|------------|-----------|
136+
| `string` | `string` |
137+
| `boolean` | `bool` |
138+
| `number` | `float64` |
139+
| `bigint` | `int64` |
140+
| `Date` | `timestamp` |
141+
| `Uint8Array` | `binary` |
142+
| `T[]` / `Array<T>` | `list<T>` |
143+
| `Set<T>` | `set<T>` |
144+
| `Map<K, V>` | `map<K, V>` |
145+
| `T \| null` / `T \| undefined` | `nullable<T>` |
146+
| Class with `@WireType` | `ref` (by tag) |
147+
148+
### Branded numeric types
149+
150+
Plain `number` maps to `float64`. For narrower types (int32, uint16, etc.),
151+
use branded types:
152+
153+
```typescript
154+
import { i32, u64, f32 } from '@aster-rpc/aster';
155+
156+
@WireType("myapp/Metrics")
157+
class Metrics {
158+
count: i32 = 0; // → int32
159+
total_bytes: u64 = 0n; // → uint64
160+
avg_latency: f32 = 0; // → float32
161+
}
162+
```
163+
164+
The scanner detects branded types via their phantom property and maps them
165+
to the correct wire primitive.
166+
167+
## Runtime fallback
168+
169+
If `aster-rpc.generated.js` is not found (e.g. during rapid prototyping),
170+
the runtime falls back to reflection: instantiating each wire type class
171+
and inspecting `Object.keys()`. This works for simple cases but has
172+
limitations:
173+
174+
- Empty arrays don't reveal their element type.
175+
- Optional/nullable nested types aren't recursed.
176+
- Non-default-constructible classes can't be introspected.
177+
- Contract identity hashes are zeroed (cross-language calls won't match).
178+
179+
A console warning fires once per class on the fallback path. For
180+
production use, always run `aster-gen`.

docs/quickstart/mission-control.mdx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ class MissionControl {
5353
```
5454

5555
```bash
56-
bun run control.ts # that's the server
56+
npx aster-gen # generate type metadata (one-time build step)
57+
node control.ts # that's the server
5758
aster shell aster1Qm... # that's the client — tab completion, typed responses
5859
```
5960

@@ -221,7 +222,9 @@ aster shell aster1Qm...
221222

222223
```typescript
223224
// control.ts
224-
import { AsterServer, Service, Rpc, WireType } from '@aster-rpc/aster';
225+
import {
226+
AsterServer, Service, Rpc, WireType,
227+
} from '@aster-rpc/aster';
225228

226229
@WireType("mission/StatusRequest")
227230
class StatusRequest {
@@ -260,8 +263,11 @@ main();
260263
```
261264

262265
```bash
266+
# Generate type metadata (run once, re-run after changing types/methods)
267+
npx aster-gen
268+
263269
# Start the control plane
264-
bun run control.ts
270+
node control.ts
265271
# → aster1Qmxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
266272
# ^^^^^^^ this is your control plane's public-key address — copy it for the next step
267273
```
@@ -528,10 +534,11 @@ async function main() {
528534
main();
529535
```
530536

531-
**Restart the service.** Stop the previous run with `Ctrl+C`, then start the new version:
537+
**Restart the service.** Stop the previous run with `Ctrl+C`, re-run the scanner and start the new version:
532538

533539
```bash
534-
bun run control.ts
540+
npx aster-gen
541+
node control.ts
535542
# → aster1Qmxxxxxxxx... ← this is a NEW address; the old one is gone
536543
```
537544

@@ -948,7 +955,7 @@ import {
948955
} from '@aster-rpc/aster';
949956
```
950957

951-
**Restart `control.ts`** (`Ctrl+C` then `bun run control.ts`) and copy the new address.
958+
**Re-run `npx aster-gen`** (to pick up the new method), then restart `control.ts` (`Ctrl+C` then `node control.ts`) and copy the new address.
952959

953960
Save this as `metrics.ts` in a **second terminal** to push 10,000 metric points:
954961

@@ -1166,14 +1173,20 @@ class AgentSession {
11661173

11671174
@BidiStream()
11681175
async *runCommand(commands: AsyncIterable<Command>): AsyncGenerator<CommandResult> {
1176+
const { execFile } = await import('node:child_process');
1177+
const { promisify } = await import('node:util');
1178+
const exec = promisify(execFile);
11691179
for await (const cmd of commands) {
1170-
const proc = Bun.spawn(["sh", "-c", cmd.command], {
1171-
stdout: "pipe", stderr: "pipe",
1172-
});
1173-
const stdout = await new Response(proc.stdout).text();
1174-
const stderr = await new Response(proc.stderr).text();
1175-
const exit_code = await proc.exited;
1176-
yield new CommandResult({ stdout, stderr, exit_code });
1180+
try {
1181+
const { stdout, stderr } = await exec("sh", ["-c", cmd.command]);
1182+
yield new CommandResult({ stdout, stderr, exit_code: 0 });
1183+
} catch (e: any) {
1184+
yield new CommandResult({
1185+
stdout: e.stdout ?? "",
1186+
stderr: e.stderr ?? e.message,
1187+
exit_code: e.code ?? 1,
1188+
});
1189+
}
11771190
}
11781191
}
11791192
}

docs/quickstart/python.mdx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ export class HelloService {
119119
}
120120
```
121121

122-
Two decorators: `@Service` (marks the class as an Aster service), `@Rpc` (marks a method as a callable endpoint). No schema files, no code generation, no base classes. Wire tags are auto-derived from the class names; for production cross-language services you'll add explicit `@WireType("ns/Type")` decorators -- see [Defining Services and Types](/docs/guides/services).
122+
Two decorators: `@Service` (marks the class as an Aster service), `@Rpc` (marks a method as a callable endpoint). No schema files, no base classes. Wire tags are auto-derived from the class names; for production cross-language services you'll add explicit `@WireType("ns/Type")` decorators -- see [Defining Services and Types](/docs/guides/services).
123+
124+
Before running, generate type metadata with `npx aster-gen` — this reads your TypeScript source and emits `aster-rpc.generated.ts`, which `AsterServer` auto-imports on startup. See [TypeScript Build Setup](/docs/guides/typescript-build-setup) for details and bundler plugin options.
123125

124126
</TabItem>
125127
</LanguageTabs>
@@ -155,7 +157,11 @@ python producer.py
155157
</TabItem>
156158
<TabItem value="typescript" label="TypeScript">
157159

158-
Create `producer.ts`:
160+
Generate type metadata, then create `producer.ts`:
161+
162+
```bash
163+
npx aster-gen
164+
```
159165

160166
```typescript
161167
import { AsterServer } from '@aster-rpc/aster';
@@ -170,7 +176,7 @@ await server.serve();
170176
```
171177

172178
```bash
173-
bun run producer.ts
179+
node producer.ts
174180
```
175181

176182
</TabItem>
@@ -251,12 +257,12 @@ Run it, passing the producer's address:
251257

252258
```bash
253259
# macOS / Linux
254-
ASTER_ENDPOINT_ADDR=<paste from producer output> bun run consumer.ts
260+
ASTER_ENDPOINT_ADDR=<paste from producer output> node consumer.ts
255261
```
256262

257263
```powershell
258264
# Windows (PowerShell)
259-
$env:ASTER_ENDPOINT_ADDR="<paste from producer output>"; bun run consumer.ts
265+
$env:ASTER_ENDPOINT_ADDR="<paste from producer output>"; node consumer.ts
260266
```
261267

262268
</TabItem>

0 commit comments

Comments
 (0)