快速开始

gotsx 是一个 Go 模块。应用 = 一个 Go 包(路由、宿主模块、动作) + 一个 app/ 目录(方言写的页面、组件、岛)。编译器把 app/ 变成 gen/ 里的 Go 和 JS, 然后 go build

1. 创建应用

一条 go install、一条 gotsx new, 出来的应用直接能跑

终端
go install github.com/childrentime/gotsx/cmd/gotsx@latest
gotsx new hello && cd hello   # scaffold an app in its own Go module (host module, page, island, action, tsconfig)
gotsx dev                     # compile → go build → run; rebuild + browser reload on change
gotsx check                   # type-check only — the same diagnostics the editor LSP shows
go build -o hello .           # one self-contained binary
gotsx dev 会先跑 cmd/hostgen 生成宿主类型, 再编译方言, 再 go build 并启动; 之后监视 app/, 改动后约 2 秒重新上线, 浏览器自动刷新。编译失败时旧版本继续运行。仓库自带的示例用 make dev-example / dev-shop

2. 目录约定

example/
hello/
├── go.mod               # module hello · require github.com/childrentime/gotsx  (gotsx.json is optional)
├── tsconfig.json        # editor support: app/.gen/gotsx.d.ts + host.d.ts are generated
├── main.go              # gotsx.Serve(gotsx.Options{Routes: gen.Routes, ...})
├── host/host.go         # host module (Go)
├── cmd/hostgen/         # reflect host.Registry → app/.gen/host.d.ts + host.json
├── app/
│   ├── pages/           # file routing: index.server.tsx → /, p/[id].server.tsx → /p/{id}, docs/[...slug].server.tsx → catch-all
│   ├── components/      # *.server.tsx → Go only
│   ├── islands/         # *.client.tsx → Go (SSR) + JS (signals)
│   ├── ui/              # no suffix = shared, compiled to both
│   └── tailwind.css     # present → Tailwind runs
├── public/              # static files → /public/*
└── gen/                 # generated output: *_gen.go, routes_gen.go, client/*.js

后缀决定编译目标: .server.tsx 只编 Go, .client.tsx 编 Go + JS(岛), 无后缀是共享组件(两端都编)。pages/ 下的文件是路由, [id] 是路径参数, [...slug] 是 catch-all。页面里 redirect() / notFound() 可以中断渲染。

3. 写一个页面

页面是 export default 的组件, props 固定是 PageProps

app/pages/index.server.tsx
import type { PageProps } from "gotsx";
import { models } from "host:data";        // Go-backed, zero marshalling
import Layout from "../components/Layout.server";
import ModelCard from "../components/ModelCard.server";

export default function Home({ query }: PageProps) {
  const q = query.q ?? "";
  const list = models.search(q);           // synchronous: concurrency from goroutines
  return (
    <Layout title="Products">
      <div class="grid">{list.map((m) => <ModelCard model={m} />)}</div>
      {list.length === 0 && <p class="empty">No matching products</p>}
    </Layout>
  );
}

注意没有 async: 宿主调用是同步的, 请求之间的并发由 goroutine 提供。

4. 写一个岛

需要交互的部分放进 .client.tsx

app/islands/Counter.client.tsx
import { useState, useEffect } from "gotsx";

export default function Counter({ start }: { start: number }) {
  const [n, setN] = useState(start);
  const double = n * 2;                    // depends on n → auto memo
  useEffect(() => { console.log(n); });    // JS backend only
  return (
    <button onClick={() => setN(n + 1)}>
      {n} ×2 = {double}{n > 4 && <b> 🔥</b>}
    </button>
  );
}

在服务端组件里像普通组件一样用: <Counter start={0} />。props 必须可 JSON 序列化(它会进 HTML 属性), 岛不接受 children。

5. 暴露 Go 能力

宿主模块 = 一个 Go 值; 字段按 json tag、方法首字母小写映射到方言

host/host.go
// host/host.go — the host module: the other side of import { models } from "host:data"
type ModelStore struct{ items []Model }

func (s *ModelStore) Search(q string) []Model {}
func (s *ModelStore) Get(id string) (Model, error) {   // error → 404 at the request layerreturn Model{}, fmt.Errorf("%w: %s", gotsx.ErrNotFound, id)
}

var Registry = map[string]gotsx.HostModule{
	"data": {Value: &DataModule{Models: store}, Go: "host.Data"},
}
app/.gen/host.d.ts(生成)
// app/.gen/host.d.ts — reflected from Go; what TSX sees = what Go exposes
declare module "host:data" {
  export interface Model { id: string; title: string; likes: number; tags: string[] }
  export interface ModelStore {
    search(arg0: string): Model[];
    get(arg0: string): Model;
  }
  export const models: ModelStore;
}

编译后 models.search(q) 就是 host.Data.Models.Search(q): 没有编组, 没有反射。Go 的 int 和方言的 number 自动转换; 返回 error 的方法, error 变成 panic 由请求层 recover, 包了 gotsx.ErrNotFound 的回 404。

6. 从岛回到 Go: 类型化 action

把 Go 方法列进 Actions, 岛里直接 import 并 await —— HTTP 调用的两端由编译器生成

host/host.go
// host/host.go — list the methods islands may call; hostgen types them as Promise<T>
func (d *DataModule) Like(id string) (int, error) { return d.Models.Like(id) }
func (d *DataModule) Rename(req *gotsx.Req, id, title string) error {} // *gotsx.Req: session / cookies

var Registry = map[string]gotsx.HostModule{
	"data": {Value: Data, Go: "host.Data", Actions: []string{"Like", "Rename"}},
}
app/islands/LikeButton.client.tsx
// app/islands/LikeButton.client.tsx — a typed action: no fetch, no untyped JSON
import { like } from "host:data";              // like(id: string): Promise<number>

export default function LikeButton({ id, likes }: { id: string; likes: number }) {
  const [n, setN] = useState(likes);
  return <button onClick={async () => setN(await like(id))}>{n}</button>;
}
// main.go: gotsx.Options{HostActions: gen.HostActions, …}
// errors: gotsx.Invalid(fields) → 422 (e.fields), gotsx.ErrNotFound → 404, else 500 — the call throws an Error with e.status

会话、flash 消息和 CSRF token 一并提供: 带 *gotsx.Req 参数的 action 可以读写签名会话; 页面拿到 props.sessionprops.flashprops.csrf(给经典表单用)。页面还可以导出 meta(props): Meta, 由布局渲染进 <head>。

7. 样式: Tailwind

有 app/tailwind.css 就会在每次构建时跑 Tailwind standalone CLI

app/tailwind.css
/* app/tailwind.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme { --color-brand-600: #1557d6; }

class="..." 在方言里就是普通字符串, Tailwind 构建期扫描 app/**/*.tsx 生成 public/tailwind.css。二进制查找顺序: $GOTSX_TAILWIND → 仓库 .tools/tailwindcss → PATH。同样不需要 Node。这个站的每一个 class 都是这么来的。