Self-hosted live demo runner (replaces CodeSandbox; retires render-ms)#37
Open
qunabu wants to merge 26 commits into
Open
Self-hosted live demo runner (replaces CodeSandbox; retires render-ms)#37qunabu wants to merge 26 commits into
qunabu wants to merge 26 commits into
Conversation
Deliverable 1 of the self-hosted demo runner that replaces CodeSandbox and, later, the render-ms redirect microservice. - config/frameworks.json: single source of truth (tier, wrappers, dev/build commands, ports) for all 13 examples. Angular is Tier 2 (needs a real Node dev server). 7 Tier-1 (Sandpack) / 6 Tier-2 (Cloudflare Sandbox containers). - pipeline/import.mjs: deterministic catalog importer; excludes build artifacts, lockfiles, CodeSandbox metadata, binaries; validates entries. - catalog.json: all 13 examples normalized to starting templates. - packages/runtime: DemoRuntime interface, resolveRuntime, and applyHandsontableVersion (ported from render-ms; pins core + wrapper in lockstep, never touches @handsontable/pikaday; pkg.pr.new supported). - packages/editor-shell: branding theme tokens (white-label). - pnpm workspace, tsconfig base, .env.example, docs/architecture.md.
…side frameworks Deliverable 2. - packages/runtime/src/sandpack.ts: SandpackRuntime implementing DemoRuntime over @codesandbox/sandpack-client (classic in-browser bundler). White-label (showOpenInCodeSandbox:false); Sandpack option/setup types derived from the loader signature; exported via the ./sandpack subpath so DOM code never reaches the sharing Worker. Applies applyHandsontableVersion before mount; writeFile streams edits via updateSandbox. - config/frameworks.json + catalog: sandpackEnvironment per Tier-1 example (parcel / create-react-app[-typescript] / vue-cli). - version.ts: fix expandPartialNumericSemver under noUncheckedIndexedAccess. - Verified from compiled dist: version validation (semver, npm partials, major cap, pkg.pr.new id/URL) and lockstep pinning are correct and pure.
Deliverable 3. - packages/editor-shell: framework-agnostic React editor. EditorShell composes a file tree, CodeMirror 6 code editor (language by extension, dark theme), a single preview-iframe slot, and a toolbar (framework tag, Handsontable version picker, Save, Share) styled from theme.ts. White-label: our logo only. The shell binds only to props/callbacks — no knowledge of Sandpack vs container. Re-keys the editor on active-file change so buffers swap correctly. - apps/authoring: Vite+React app assembling the shell with a DemoRuntime. Loads catalog.json, picks an example, mounts SandpackRuntime for Tier 1 with version dispatch, streams edits via writeFile. Tier-2 shows an editing-enabled/ preview-disabled placeholder until Deliverable 4. - Verified live in a browser against the hosted bundler: React (Vite, TS) and Vue 3 render a live Handsontable grid at v18.0.0; editing a column header updates the preview live; switching examples swaps files/engine correctly. Cloudflare Access gates this app at the edge at deploy time (no in-app auth).
Plan/design updates per new requirements: - Auth is now the shared Handsontable Google login broker (@handsontable.com only, no passwords), superseding Cloudflare Access. Worker validates the per-user token server-side via /broker/userinfo; created_by = verified email. Deploy on handsontable-sandbox.workers.dev (a broker allowed return host). - Internal per-user demos + fork flow: fork any existing demo (catalog starter or saved demo), add title + description, save -> unique /d/:id client URL. D1 schema gains description + forked_from (+ index); created_by powers 'My demos'. - Docs-only embed: GET /embed/:id read-only, locked to handsontable.com via CSP frame-ancestors (+ Sec-Fetch/Origin checks) so no other site can embed it. - .env.example: LOGIN_BROKER_URL, EMBED_ALLOWED_ANCESTORS, workers.dev origins.
Generating/copying the embeddable /embed/:id URL is shown only to signed-in internal users (broker session); serving the embed stays public but frame-locked to handsontable.com.
Provisioned in account 15111272c53ed0aaf84a908f0c9c7f8b (NOT the sandbox): - D1 handsontable-demos (5fc0854f-...) region EEUR, schema 0001_init.sql applied - KV handsontable-demos-cache (6620876d...) - R2 handsontable-demos (created after R2 enabled on the account) - workers/api/wrangler.jsonc: real D1/KV/R2 bindings + account_id + broker/embed vars. - workers/api/migrations/0001_init.sql: demos (+ description, forked_from) and build_cache tables, indexes. - docs/cloudflare-resources.md: authoritative resource inventory. - .env.example / architecture.md: point at the main account + its workers.dev subdomain instead of the sandbox. Resources are managed with wrangler (the Cloudflare MCP is bound to the sandbox account, so it is not used here). Stray D1/KV mistakenly created in the sandbox were deleted.
…roven end-to-end Deliverable 4 (first framework proven; other 4 Tier-2 examples follow). - workers/api: orchestration Worker on the Cloudflare Sandbox SDK. Per-session container runs the real dev server; proxyToSandbox() proxies the preview URL incl. WebSocket/HMR. Routes: POST /api/session (write files -> start dev -> exposePort -> previewUrl), POST /api/session/:id/file (stream edit), DELETE (destroy). CORS for the authoring app. Shallow Sandbox types avoid TS2589. - containers/remix: Dockerfile (FROM cloudflare/sandbox:0.12.3, deps baked, EXPOSE 5173) + generated build-context package.json. - scripts/prepare-container.mjs: build the container context from the catalog. - packages/runtime/container.ts: ContainerRuntime implements DemoRuntime over the session API (iframe.src = preview URL; debounced writeFile -> HMR). Exposed via the ./container subpath. - apps/authoring: Tier-2 now uses ContainerRuntime via the same shell as Tier-1. Verified locally (wrangler dev + Docker): Remix boots in a Sandbox container, renders a live Handsontable grid through the preview URL, and edits made in the unified editor HMR into the preview within seconds — the author cannot tell it is a different engine than the Tier-1 React demo.
- config/frameworks.json: next-shadcn.js gets its own container (deps differ from
Next 13). scripts/prepare-container.mjs now generates a Dockerfile +
package.json per Tier-2 framework and workers/api/src/frameworks.generated.ts
(framework -> dev command, port, DO binding) from the catalog.
- containers/{remix,angular,next,next-shadcn,astro,nuxt}: baked-deps images.
- workers/api: one Sandbox subclass + DO binding + image per framework; session
ids encode the container (<container>--<uuid>) so file/delete route to the
right image. Dev commands bind 0.0.0.0 at the framework's port.
- docs/adr: 18 Architecture Decision Records (0001–0018) + index.
Container images build-verified via docker build: astro, nuxt (OK); next,
angular, next-shadcn building. Remix already proven live end-to-end (ADR-0002).
- workers/api/share.ts: snapshotter runs the real framework build in a generic builder container (npm install + buildCommand with node_modules/.bin on PATH), collects the static output, uploads to R2, records metadata in D1, mints a short id. Builds immutable + deduped per (framework, ht_version, files_hash) via build_cache. Serves /d/:id and /embed/:id from R2. - workers/api/auth.ts: broker-token validation (@handsontable.com only) with a local DEV_AUTH_EMAIL bypass (gitignored .dev.vars). - Endpoints: POST /api/demos (fork->build->R2->id), GET /api/demos (My demos), GET /api/demos/:id (public, cached, 410 if revoked), PATCH/DELETE (owner), GET /d/:id[/*] (immutable, X-Frame-Options DENY), GET /embed/:id[/*] (frame-ancestors handsontable.com only). Trailing-slash redirect for relative asset paths. - containers/builder: generic Node image for the snapshotter. BUILD_CONFIG for all 13 examples generated by prepare-container.mjs. Verified locally (wrangler dev + Docker): shared a javascript demo -> built in container -> /d/:id/ renders the prebuilt static grid from R2; My demos lists it; embed sends frame-ancestors; revoke -> 410.
Worker live at handsontable-demos-api.handsoncode.workers.dev (main account). 7 Sandbox container apps created; D1/KV/R2 bound; migrations applied. Static sharing + viewer work on workers.dev now; live Tier-2 sessions still need a wildcard custom domain (ADR-0011).
- apps/authoring/auth.ts: Handsontable login broker client (Sign in with Handsontable, @handsontable.com only; token in sessionStorage; Log out). Matches the publish-app skill's login experience. Dev bypass via VITE_DEV_USER. - App gated behind login; shows signed-in email + Log out. Share opens a dialog (title + description) -> POST /api/demos -> client link + docs embed URL with Copy buttons (login-gated by construction). 'My demos' side panel lists the user's demos with View / Fork. - workers/api: PREVIEW_HOST for wildcard preview URLs in prod (ADR-0011); GET /api/demos/:id/source (auth) for forking a saved demo; share.ts stores a source snapshot (never served as a public asset) and strips type-check steps (tsc/vue-tsc) from build commands so static snapshots don't fail on types. Verified locally end-to-end in the browser: signed-in editor -> edit React demo -> Share (title/description) -> real container build -> client link + embed URL; /d/:id renders the prebuilt grid. Requires broker allowlist for handsoncode.workers.dev (done in hot-mcp, pending broker redeploy).
- D8 render-ms compatibility shim: GET /codesandbox-vm, /codesandbox-browser, and /r/:framework map old public deep links (example-dir + handsontable-version) to a build-or-reuse PUBLIC static render with a deterministic /d/:id (migrate.ts; version 'latest' resolved via npm). Reuses applyHandsontableVersion from the runtime package. - Fix: idx_demos_buildkey is now NON-unique (migration 0002; artifact dedupe lives in build_cache) so two users can share identical files independently; createDemo upserts by id (deterministic ids for shim renders). - Friendly root index at / (was a bare 404 — this is the API worker). - instance_type standard -> standard-1 (deprecation). - apps/authoring: wrangler.jsonc (static SPA Workers Assets) + deployed to handsontable-demos-authoring.handsoncode.workers.dev. Prod build omits the dev login bypass. - docs/: run-and-deploy, self-host-bundler, create-and-share-a-demo (non-technical). Verified in prod: API root 200 (friendly index), authoring app serves; API redeployed (version with shim). render-ms links build-on-first-hit then cache.
Iterates the catalog × the latest N stable Handsontable versions (majors 15-19, from npm) and hits the deployed /r/:framework render endpoint so each is built and cached in R2 (reusing the prod build+cache path). Configurable via WARM_API_BASE / WARM_VERSIONS / WARM_EXAMPLES / WARM_CONCURRENCY; optional WARM_TIER2_CONTAINERS=1 warms one container per Tier-2 framework.
- Fork of a different-framework demo was clobbered by the entry-reset effect (High). Removed that effect; a new loadWorkspace() replaces framework + files + lineage atomically. Example dropdown now uses selectExample(). - Fork of a same-framework demo didn't remount the runtime, so the preview kept the old project (Medium). Added a mountGen counter to the mount effect deps so a wholesale file replacement always remounts. - forkedFrom lineage was set before the source fetch succeeded (Medium). openFork now fetches first and only loads/sets lineage on success; failure leaves the workspace untouched and surfaces an error.
Contributor
Author
|
Fixed all three Bugbot findings in the fork flow in commit b256e05:
Authoring app redeployed with the fix. |
- Authoring editor/playground is now PUBLIC; sign-in only gates creating a client demo (Share) and 'My demos'. Anonymous visitors get a Sign-in control; Share when signed out starts the broker login. (ADR-0007 amended.) - Version picker uses REAL published versions via new GET /api/versions (npm-backed, KV-cached) + a custom-version input for nightlies / pkg.pr.new builds (e.g. 0.0.0-next-<sha>-<date>). Removes the old hardcoded invalid list. - workers/api: PREVIEW_HOST=demos.handsontable.com + wildcard route *.demos.handsontable.com/* (workers_dev kept enabled) for live Tier-2 preview. Deployed: API + authoring. NOTE: live Tier-2 preview still needs a TLS cert covering *.demos.handsontable.com (Universal SSL does not cover 2nd-level wildcards) — order via Advanced Certificate Manager.
Example + version are read from the URL on load and written back on change (history.replaceState), so links reopen the app at that example/version. Login return_to now includes the query so a deep link survives the broker round-trip.
…-shims - Branding: Handsontable blue #1A42E8 accent (no green); real logo SVG inlined (self-hosted asset) replaces the 'Handsontable Demos' title + placeholder mark. - Toolbar gated by auth: public users see the version dropdown + a 'Fork this demo' button only; Save/Share/custom-version appear after sign-in (login only gates creating a client demo). Removed meaningless 'Tier N' from the picker. - My demos: View / Edit(fork) / Delete (revoke). - SandpackRuntime.ensureSandpackDeps: inject transitive runtime deps the in-browser bundler doesn't resolve (@swc/helpers for handsontable; pikaday + moment + jquery for @handsontable/pikaday). Fixes the 'cannot find module' crashes; kept the free hosted bundler (self-host has a v18 stack overflow). Known follow-up: react-js/ant-design compile but render blank on the classic bundler (root-level entry vs create-react-app /src convention) — needs per-example entry normalization or containers.
…ck html/env fixes - Changing the Handsontable version now re-pins /package.json in the editor (handsontable + framework wrapper in lockstep via applyHandsontableVersion), so what you see matches what the bundler/container installs. - SandpackRuntime: normalize create-react-app -> create-react-app-typescript env; strip <base> tags from HTML (breaks the preview iframe); + moment shim. Fixes javascript rendering; react/vue/ts render. react-js/ant-design still blank on the classic bundler (tracked separately).
proxyToSandbox() hard-requires a DO namespace literally named 'Sandbox'; the per-framework namespaces (SANDBOX_REMIX, ...) made env.Sandbox undefined, so container previews returned 'Proxy routing error'. Collapse to ONE Sandbox class backed by a generic image (containers/live) that installs each demo's deps at session start and runs the real dev server; builder keeps its own class. Migration v2 deletes the 6 framework classes and adds Sandbox. Session ids are sanitized for the preview hostname; port-wait bumped to 45s (install + boot). Verified in PRODUCTION: a Remix session renders a live Handsontable grid over https://<port>-<id>-<token>.demos.handsontable.com/ (wildcard ACM cert active).
…ample1 CSS
- Tier-2 sessions now boot asynchronously: POST /api/session returns immediately
after starting a background install+dev process (logged to /tmp/boot.log) and
exposing the port. New GET /api/session/:id/status?port= returns { ready, log }.
ContainerRuntime polls it, streams progress via onProgress, and only points the
iframe at the preview when the dev server is up. PreviewPane shows a spinner +
live boot log so Angular/Next no longer look frozen on 'Booting…'.
- examples/react-js: move entry to /src/index.jsx + drop <base href='.'> so it
matches the working react example structure.
- examples/example1: remove obsolete 'handsontable/dist/handsontable.full.min.css'
import (not needed since 17.1).
…lane)
exposePort(3000) is rejected ('sandbox control plane'). angular->4200,
next/next-shadcn/nuxt->3001, remix 5173/astro 4321 unchanged. EXPOSE updated.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6e5b398. Configure here.
…rsion R2 cache
- Route react-js + ant-design through the container engine (engine:'container'
in catalog; App/runtimes select by engine, not tier). They render exactly as
authored (real vite dev).
- Fast, budget-wise Tier-2 boots:
* one generic live image bakes each framework's node_modules into /baked/<key>
(scripts/prepare-container.mjs); session hardlink-copies it so a
default-version boot skips npm install. No idle warm pool.
* per-(framework,version) node_modules cache in R2 via GET/PUT
/api/nmcache/:framework/:version — the container restores a cached tarball
when available, else installs (seeded from baked) and uploads it for next
time. Boot script: cache -> baked+install -> cache upload -> dev.
- Cleanup: single Sandbox model; removed per-framework binding/container config.
… phase - Fork action shows only for signed-in users (anonymous visitors browse/edit/ preview and sign in from the top bar). - ContainerRuntime keeps the loading indicator past dev-server-ready through the client render phase: on iframe load it waits a short grace before firing ready (SPAs like Angular bootstrap after the HTML loads), with a hard fallback. Progress shows 'Dev server ready — rendering the demo…'.
…curl noise The per-version cache upload was on the critical path, delaying the dev server start. Move the tar+PUT into a background subshell so the dev server starts right after install; silence the expected 404 on a cache miss.
…utes
API worker claims demos.handsontable.com/{api,d,embed,r,codesandbox-*} + keeps the
*.demos wildcard (previews); authoring app owns the catch-all demos.handsontable.com/*
(more-specific API routes win). Authoring built with VITE_API_BASE=
https://demos.handsontable.com so client/embed links are same-origin + clean.
Needs a proxied DNS record for demos.handsontable.com to resolve.
Tier-2 frameworks rebuild server-side (a few seconds) after an edit, unlike instant in-browser Tier-1. Show an 'Applying changes…' pill on the preview after a container edit so the rebuild latency is visible (edits do reflect; Angular's esbuild rebuild just isn't instant).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Self-hosted system that renders every Handsontable example live at any version, lets the internal team edit demos and share permanent client links, and can embed demos on the docs — replacing CodeSandbox and, eventually,
render-ms. All code lives underrunner/(pnpm monorepo).render-ms/is untouched; removal is a follow-up.Architecture — one UX, two engines behind a
DemoRuntimeadapterDemoRuntime; the author can't tell which engine runs. No Nodebox / WebContainers.What's included
config/frameworks.json+pipeline/import.mjs→catalog.json(all 13 examples migrated).packages/runtime—DemoRuntime,SandpackRuntime,ContainerRuntime,resolveRuntime, andapplyHandsontableVersion(ported from render-ms: semver / npm partials / pkg.pr.new; lockstep wrapper pinning; majors 15–19).packages/editor-shell— white-label editor (file tree, CodeMirror 6, live preview, version picker, Share) +theme.ts.apps/authoring— Vite/React app behind the Handsontable Google login broker (@handsontable.com); fork → title/description → Share; "My demos"; copy client link + docs embed URL.workers/api— orchestration + sharing Worker: Tier-2 sessions (Sandbox SDK,proxyToSandboxHMR),POST/GET/PATCH/DELETE /api/demos,/d/:id,/embed/:id(frame-ancestors handsontable.com), render-ms shim (/codesandbox-vm,/codesandbox-browser,/r/:framework).containers/*— one baked-deps image per Tier-2 framework + a generic builder for the snapshotter.pipeline/scripts— catalog importer, container prep,warm.ts.docs/— architecture, run-and-deploy, self-host-bundler, create-and-share-a-demo, cloudflare-resources, and 18 ADRs.Deployed (main Handsontable account
15111272…)https://handsontable-demos-api.handsoncode.workers.devhttps://handsontable-demos-authoring.handsoncode.workers.devhandsontable-demos, KVhandsontable-demos-cache, R2handsontable-demos.Verified
/d/:idrenders prebuilt static; embed sendsframe-ancestors; revoke → 410./codesandbox-vm?example-dir=javascript&handsontable-version=18.0.0→/d/r-javascript-18-0-0/).Follow-ups (need ops)
hot-mcplogin broker (branchbroker-allow-handsoncode-workers-dev) so real logins work fromhandsoncode.workers.dev.*.demos.handsontable.comwildcard DNS → API worker for live Tier-2 preview (ADR-0011), then setPREVIEW_HOST+ redeploy. Static shares/embeds work without it.render-msafter parity confirmation.Note
Low Risk
Single-line demo styling change in a non-production example; risk is mainly an unstyled Handsontable grid if no replacement CSS path exists.
Overview
Removes the
handsontable/dist/handsontable.full.min.cssside-effect import from the Vanilla (Parcel)example1entry (examples/example1/index.ts), so the demo no longer pulls the monolithic dist stylesheet through JS.The example still uses
handsontable/baseandregisterAllModules(); verify that grid/theme styles still come from somewhere else (e.g.index.html→styles.cssor explicithandsontable/styles/*imports like other examples), since this diff only deletes the import.Reviewed by Cursor Bugbot for commit 338d161. Bugbot is set up for automated code reviews on this repo. Configure here.