Serve sculpt studio at /sculpt

Keep existing viewer URLs compatible and test embedded pages, assets, and isolation headers.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-09-28 10:25:02 +00:00
co-authored by heaust
parent 347b676cf1
commit d930d8172f
4 changed files with 44 additions and 2 deletions
+2
View File
@@ -89,6 +89,8 @@ func (app *application) routes() http.Handler {
mux.HandleFunc("/playground/", app.servePlayground)
mux.HandleFunc("/view/gltf", app.servePage("gltf.html"))
mux.HandleFunc("/view/gltf/", app.servePage("gltf.html"))
mux.HandleFunc("/sculpt", app.servePage("assets/sculpt/index.html"))
mux.HandleFunc("/sculpt/", app.servePage("assets/sculpt/index.html"))
mux.HandleFunc("/view/sculpt", app.servePage("assets/sculpt/index.html"))
mux.HandleFunc("/view/sculpt/", app.servePage("assets/sculpt/index.html"))
mux.HandleFunc("/", app.serveHome)
+40
View File
@@ -217,6 +217,46 @@ func TestSavedPlaygroundPageRoute(t *testing.T) {
}
}
func TestSculptRoutesAndAssets(t *testing.T) {
routes := testApplication(t).routes()
for _, path := range []string{"/sculpt", "/sculpt/", "/view/sculpt", "/view/sculpt/", "/assets/sculpt/app.js", "/assets/sculpt/styles.css"} {
t.Run(path, func(t *testing.T) {
response := httptest.NewRecorder()
routes.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))
if response.Code != http.StatusOK {
t.Fatalf("status = %d", response.Code)
}
for name, expected := range map[string]string{
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
} {
if got := response.Header().Get(name); got != expected {
t.Fatalf("%s = %q, want %q", name, got, expected)
}
}
switch path {
case "/assets/sculpt/app.js", "/assets/sculpt/styles.css":
expected := "text/javascript; charset=utf-8"
if filepath.Ext(path) == ".css" {
expected = "text/css; charset=utf-8"
}
if response.Header().Get("Content-Type") != expected || response.Body.Len() == 0 {
t.Fatalf("missing asset or wrong content type: %q", response.Header().Get("Content-Type"))
}
default:
if got := response.Header().Get("Content-Type"); got != "text/html; charset=utf-8" {
t.Fatalf("content type = %q", got)
}
for _, content := range []string{"<title>Sculpt — Yawn</title>", `src="/assets/sculpt/app.js"`, `href="/assets/sculpt/styles.css"`, `"@yawn/core": "/pkg/core.js"`} {
if !bytes.Contains(response.Body.Bytes(), []byte(content)) {
t.Fatalf("sculpt page missing %q", content)
}
}
}
})
}
}
func TestGltfViewerPageRoute(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/view/gltf", nil)
response := httptest.NewRecorder()
+1 -1
View File
@@ -1,6 +1,6 @@
# Yawn Sculpt
Open `/view/sculpt` on the Yawn server (`npm run server`). All example code lives here; the only integration outside this folder is the embedded-page route in `server/app.go`. Rendering imports **only `@yawn/core`**. No handles, third-party scene graph, or remote document storage.
Open `/sculpt` on the Yawn server (`npm run server`); `/view/sculpt` remains a compatibility alias. The production Docker image embeds this example, so the existing main-branch deployment poller publishes it at `/sculpt` with each deployment. All example code lives here; the only integration outside this folder is the embedded-page route in `server/app.go`. Rendering imports **only `@yawn/core`**. No handles, third-party scene graph, or remote document storage.
## Workflow
+1 -1
View File
@@ -1,4 +1,4 @@
// Run explicitly in a fresh /view/sculpt page:
// Run explicitly in a fresh /sculpt page:
// await (await import('/assets/sculpt/browser-check.js')).check()
// Synthetic input checks logic, not physical pen feel. Hooks are always restored.
import { Renderer } from "./renderer.js";