Studio architecture (DEVSTACK merge)

View raw
On this page

Studio (/studio) คือแอป DEVSTACK v0.2.0 ของทีม (Vite + React) ที่เรารวมเข้ากับ devstack.bid ใน v0.8.0-rc หน้านี้อธิบายว่าเราต่อมันเข้ากับ Worker เดิมอย่างไร ทำไมเลือกวิธีนี้ และอะไรที่ต้องระวัง

ทำไมเป็น sub-app ไม่ใช่หน้า Next.js#

DEVSTACK เป็น SPA ที่ต้องการ 3 อย่างที่ Next.js App Router ของเราไม่มี: Web Worker ของ Monaco, ไฟล์ WASM ของ PGlite (~10 MB) ที่โหลดผ่าน new URL(..., import.meta.url) และ header เฉพาะของตัวเอง (CSP ที่ยอมให้ compile WASM, COOP/COEP และหน้า preview ที่มี CSP แยก) ถ้ายัดเข้าไปใน Next.js เราต้องแปลง bundler จาก Vite เป็น Turbopack และเสี่ยงให้ header เหล่านี้ไหลไปถึงหน้าอื่น เราจึงเลือก:

  • โค้ดอยู่ใน studio/ (มี package.json และ lockfile ของตัวเอง) dependency หนัก ๆ (Monaco, PGlite, Sucrase, xterm, Vite 8) ไม่ปนกับ tree ของ Next.js
  • scripts/build-studio.mjs (รันใน prebuild) build เป็นไฟล์ static ลง public/studio/ แล้ว OpenNext คัดลอกเข้า .open-next/assets ตามปกติ
  • Cloudflare เสิร์ฟ /studio/* จาก static assets ก่อนถึง Worker และใส่ header จาก public/_headers บล็อก /studio/* เท่านั้น
  • API ของ Studio เป็น route handler ของ Next.js ที่ /api/studio/* ใน Worker ตัวเดิม
text
Browser /studio/ (COOP + COEP, Studio CSP)          Worker nvx-stack-builder (Next.js 16 / OpenNext)
───────────────────────────────────────────         ─────────────────────────────────────────────────
AgentChat → AgentOrchestrator ─► ProxyLLMProvider ─► POST /api/studio/llm/chat ─┐  same-origin check
   ├ DBAgent    → PGlite + pgvector (IndexedDB)      POST /api/studio/image ────┤  NVX_STUDIO_LIMITER (burst)
   ├ CodeAgent  → ProjectFs → Sucrase bundle         GET  /api/studio/health ───┤  nvx_chat_uid cookie (HMAC)
   └ DesignAgent→ image proxy → public/assets                                    │  D1 usage_counters (quota)
Preview iframe /studio/preview/ (sandbox) ⇄ SQL bridge                            ├► AI binding: STUDIO_MODEL
Export (JSZip) → source + SQL + Prisma + deploy                                   └► HF router: HF_MODEL (fallback)

Header: COOP/COEP เฉพาะ /studio#

ส่วนของเว็บ COOP COEP CSP
หน้า Next.js ทั้งหมด (/, /chat, /docs …) same-origin (มีอยู่แล้วตั้งแต่ก่อน merge) ไม่มี contentSecurityPolicy() ใน src/lib/security-headers.ts
/studio/* (static) same-origin require-corp studioContentSecurityPolicy() ใน src/lib/security-headers.ts คัดลอกไว้ใน public/_headers (มี test ตรวจว่าตรงกัน)
/studio/preview/* (หน้า preview) same-origin require-corp studioPreviewContentSecurityPolicy(): บล็อกนี้ถอด CSP ของ /studio/* และ X-Frame-Options ออกก่อน (! Content-Security-Policy, ! X-Frame-Options) แล้วใส่ของตัวเอง

CSP ของ Studio ต่างจากของเว็บหลักสองจุด: 'wasm-unsafe-eval' (PGlite compile WASM) และ frame-src 'self' (หน้า preview ของเราเอง ไม่มี frame จากภายนอก) ไม่มี 'unsafe-inline' ใน script-src เพราะ Vite build ไม่มี inline script

ตอนนี้ไม่มีอะไรใน Studio ที่ต้องใช้ cross-origin isolation (SharedArrayBuffer) แล้ว เราคง COOP/COEP ไว้เพื่อให้ Studio อยู่ใน browsing context group ของตัวเองและไม่มี resource ข้ามโดเมน ถ้าต้องการให้ Cloudflare Web Analytics ทำงานใน /studio ก็ถอด COEP ออกได้โดยไม่กระทบฟีเจอร์

ผลต่อเว็บเดิม: ไม่มี เพราะ COEP อยู่ใน /studio/* เท่านั้น รูปภาพ, Cloudflare Web Analytics, การเรียก OpenAI/HF (ฝั่งเซิร์ฟเวอร์) และหน้าอื่นทำงานเหมือนเดิม ลิงก์เข้า/ออก Studio เป็น <a> ธรรมดา (ไม่ใช่ next/link) เพื่อให้เบราว์เซอร์โหลดเอกสารใหม่และใช้ header ชุดใหม่

ข้อควรระวังใน /studio: ทุก resource ข้ามโดเมนต้องส่ง Cross-Origin-Resource-Policy หรือ CORS ถ้า Cloudflare Web Analytics แทรก beacon (static.cloudflareinsights.com) ลงใน /studio/ แล้ว beacon ไม่ส่ง CORP เบราว์เซอร์จะบล็อก script นั้น (แอปไม่พัง แต่จะมี error ใน console) ทางแก้คือยกเว้น /studio/* จาก Web Analytics ใน dashboard

Static preview (แทน WebContainers)#

DEVSTACK เดิมรัน Node.js + npm install + Vite dev server ใน WebContainers ของ StackBlitz ซึ่งเป็น runtime ปิดและต้องมี license เชิงพาณิชย์สำหรับการใช้งาน production บางแบบ เราเอาออกทั้งหมด (v0.8.0-rc.2) แล้วใช้ preview แบบ static ที่เป็น open source ทั้งหมด:

  1. studio/src/sandbox/staticBundler.ts แปลงทุกไฟล์ .js/.jsx/.ts/.tsx ใน src/ ด้วย Sucrase (MIT, JSX automatic runtime + ESM → CommonJS) เก็บ .css ไว้ inject และแปลงไฟล์ใน public/ เป็น data URL (แทน string "/assets/x.png" ในโค้ด) ใช้ entry จาก <script type="module"> ใน index.html
  2. studio/src/preview/vendor.ts คือ runtime (React 19 + ReactDOM) build เป็น IIFE ด้วย vite.preview.config.ts ออกมาเป็นไฟล์ vendor.js ในโฟลเดอร์ preview (ไฟล์ที่ได้จากการ build ราว 220 KB) Studio โหลดครั้งเดียว
  3. iframe /studio/preview/ (studio/public/preview/index.html) ใช้ sandbox="allow-scripts allow-forms allow-modals" (ไม่มี allow-same-origin จึงเป็น origin แบบ opaque) พอโหลดเสร็จจะส่ง nvx-preview:hello Studio ตอบด้วย nvx-preview:load (vendor + module map) แล้ว loader require() ขนาดเล็กในหน้านั้นรันโค้ด console และ error ถูกส่งกลับมาที่แท็บ Output
  4. SQL bridge เดิม (devstack:sql ↔ devstack:sql:result) ยังใช้ได้ Studio รับเฉพาะข้อความจาก contentWindow ของ iframe ที่มี origin "null" แล้วตรวจคำสั่งด้วย validateBridgeSql() เหมือนเดิม
  5. แก้ไฟล์แล้ว Studio build ใหม่ (debounce 250 ms) และโหลด iframe ใหม่ทั้งหน้า

สิ่งที่หายไปเมื่อเทียบกับ WebContainers: npm install แพ็กเกจอื่น, terminal shell (jsh), Node server/Vite plugin, HMR ที่เก็บ state และการเรียกเน็ตจากแอป (หน้า preview มี CSP connect-src 'none') prompt ของ CodeAgent บอกเรื่องนี้แล้ว (react และ react-dom เท่านั้น) ไฟล์ export ยังเป็นโปรเจกต์ Vite ที่รันบนเครื่องได้

LLM และภาพ: ใช้ผู้ให้บริการของเราแทน LLM_API_KEY#

DEVSTACK เดิมมี proxy ของตัวเอง (handleApiRequest()) ที่ต้องใช้ LLM_API_KEY ของผู้ให้บริการแบบ OpenAI-compatible เราตัดส่วนนั้นออก (รวมถึง Node server, Vercel functions และ Cloudflare Pages functions) แล้วให้ client เรียก src/lib/studio/route.ts แทน โดยคง wire contract เดิม (studio/src/shared/apiTypes.ts ใช้ร่วมกันทั้งสองฝั่ง):

  • POST /api/studio/llm/chat { messages, json?, maxTokens? } → { content, model } เรียก Workers AI (STUDIO_MODEL, Qwen 2.5 Coder 32B) ก่อน ถ้าล้มหรือได้คำตอบว่างจะไป Hugging Face (HF_MODEL, DeepSeek V3) ตั้ง STUDIO_PROVIDER=hf เพื่อสลับลำดับ ถ้า json: true เราเติมคำสั่งให้ตอบ JSON อย่างเดียว (client มี extractJsonObject รองรับคำตอบที่มี fence อยู่แล้ว)
  • POST /api/studio/image { prompt } → { dataUrl, model } ใช้ FLUX (WAI_IMAGE_MODEL) แล้ว client บีบอัดเป็น PNG ใน Web Worker
  • GET /api/studio/health → ความสามารถของเซิร์ฟเวอร์ + โควตาที่เหลือของ cookie นี้ และออก cookie nvx_chat_uid ถ้ายังไม่มี

ทุก POST ผ่าน: ตรวจ same-origin (Origin / Sec-Fetch-Site) → burst limiter NVX_STUDIO_LIMITER (20/นาที/IP) → อ่าน body ไม่เกิน 512 KB → ตัด quota ใน D1 (studio:uid:*, studio:ip:*, studio:global หรือ bucket ภาพของ User Chat) → เรียกโมเดล → ถ้าล้มคืน quota (refund) ข้อความ error เป็นไทย/อังกฤษและไม่มี secret

ถ้าไม่มี D1, CHAT_COOKIE_SECRET หรือทั้ง binding AI และ HF_TOKEN (เช่น next dev เปล่า ๆ) /api/studio/health ตอบ llm: false แล้ว Studio ใช้ mock agent ที่ deterministic ซึ่งทำ flow ทั้งหมดได้ครบ

Design system#

Studio ใช้ token ชุดเดียวกับเว็บ: scripts/sync-studio-brand.mjs คัดลอก src/app/tokens.css, ส่วน mascot และ signature ของ src/app/globals.css, และ component src/components/brand/mascot.tsx, terminal-signature.tsx ไปไว้ที่ studio/src/brand/ (ห้ามแก้ตรงนั้น tests/studio.test.ts จะล้มถ้าไม่ sync) ชื่อ utility เดิมของ DEVSTACK (bg-devstack-*) ถูก map เข้า token ของเราใน studio/src/index.css: พื้นดำ, surface graphite, ฟ้า #2997ff เป็น accent, ปุ่มหลักสีฟ้า #0071e3 แบบ pill, ชมพู #fb0fab สำหรับ glow ฟอนต์ Inter + IBM Plex Sans Thai + IBM Plex Mono ภาษาของ UI ใช้ key nvx-lang ใน localStorage ร่วมกับเว็บ

ขนาดและประสิทธิภาพ#

public/studio/ ราว 34.4 MB (199 ไฟล์) ใหญ่สุดคือ pglite-*.wasm 10.1 MB ต่ำกว่าเพดาน 25 MiB ต่อไฟล์ของ Workers static assets หน้าแรกโหลด JS ราว 270 KB (gzip) ส่วน Monaco (~1 MB gzip), PGlite (~3 MB), Sucrase (~200 KB) และ export engine โหลดแบบ lazy runtime ของ preview (vendor.js) ราว 220 KB ไฟล์ใน /studio/assets/* cache แบบ immutable หนึ่งปี

การทดสอบ#

  • tests/studio.test.ts (Vitest ของ Next.js): validation, same-origin, ลำดับผู้ให้บริการ, quota/refund, health ไม่มี secret, header, brand sync, ไม่มี emoji/key ใน studio/src
  • npm run studio:check: ESLint + tsc -b + Vitest ของ Studio (agents, PGlite + pgvector, export, static bundler)
  • npm run studio:e2e: Playwright flow "build a todo app" กับ preview ที่รันอยู่ (รวมการเพิ่ม todo ใน iframe แล้วเห็นแถวใหม่ใน Data Inspector)

In English#

We merged DEVSTACK as a static sub-app instead of porting it into the App Router: studio/ keeps its own Vite build and lockfile, scripts/build-studio.mjs writes it to public/studio/, and the same Worker serves it from static assets with COOP/COEP and a Studio-specific CSP scoped to /studio/* in public/_headers. The live preview is fully open source: Sucrase compiles the project in the browser and a sandboxed, network-less iframe (/studio/preview/, its own CSP) runs it with a vendored React runtime; the PGlite SQL bridge is unchanged. WebContainers (npm install, Node, shell) were removed in 0.8.0-rc.2. The original key-based proxy was replaced by Next.js route handlers (/api/studio/*) that reuse our Workers AI binding, the Hugging Face fallback, the signed nvx_chat_uid cookie, D1 quotas and a new NVX_STUDIO_LIMITER. The rest of the site keeps its headers and gets no COEP. User guide: Studio.

Source: docs/architecture/studio.md · /docs/architecture/studio.md