Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions gno.land/pkg/gnoweb/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ biome := $(node_modules)/.bin/biome
# css config
css_config_path := frontend/css/main.css

# main css config
main_css_files := $(shell find frontend/css -name '*.css')
# main css config — includes per-feature CSS pulled into the Cube CSS Block
# layer via @import from frontend/css/main.css (postcss-import resolves at
# compile time, so the final bundle and layer order are unchanged).
main_css_files := $(shell find frontend/css feature -name '*.css' 2>/dev/null)
output_maincss := $(PUBLIC_DIR)/main.css

templates_files := $(shell find . -iname '*.html')
Expand All @@ -35,11 +37,25 @@ output_static := $(patsubst $(src_dir_static)/%, $(out_dir_static)/%, $(input_st
# esbuild config
src_dir_js := frontend/js
out_dir_js := $(PUBLIC_DIR)/js
input_js := $(shell find $(src_dir_js) -name '*.ts')
# Pick up both legacy controllers under frontend/js/ and per-feature
# controllers under feature/<name>/frontend/. All compile to
# $(out_dir_js)/<basename>.js (flat output, no source-path mirroring).
input_js := $(shell find $(src_dir_js) -name '*.ts') \
$(shell find feature -path 'feature/*/frontend/*.ts' 2>/dev/null)
# Guard against silent overwrites: two features cannot ship the same
# controller-X.ts basename (they would compile to the same target).
_dup_check := $(shell echo "$(notdir $(input_js))" | tr ' ' '\n' | sort | uniq -d)
ifneq ($(_dup_check),)
$(error duplicate JS controller basenames: $(_dup_check))
endif
# Separate shared and controller files
shared_js := $(src_dir_js)/controller.ts
controller_js := $(filter-out $(shared_js),$(input_js))
output_js := $(patsubst $(src_dir_js)/%.ts,$(out_dir_js)/%.js,$(input_js))
# Flatten: any .ts (legacy or feature) → $(out_dir_js)/<basename>.js.
output_js := $(addprefix $(out_dir_js)/,$(patsubst %.ts,%.js,$(notdir $(input_js))))
# Tell make where to look for .ts sources so the %.js pattern rule below
# resolves both legacy and feature-local controllers.
vpath %.ts $(src_dir_js) $(shell find feature -path 'feature/*/frontend' -type d 2>/dev/null)

# cache
cache_dir := .cache
Expand Down Expand Up @@ -82,14 +98,21 @@ $(output_maincss): $(main_css_files) $(templates_files) $(postcss)
$(postcss) $(css_config_path) -o $@ --env production
touch $@

# Per-feature controllers live under feature/<name>/frontend/, outside
# frontend/js/. NODE_PATH points esbuild at the single frontend/node_modules
# so npm imports (e.g. "htmx.org") resolve from any source location.
node_path := $(node_modules)

ts: $(esbuild) $(output_js)
# Build shared chunk first (always loaded)
$(out_dir_js)/controller.js: $(shared_js) $(esbuild)
NODE_ENV=production $(esbuild) $< --log-level=error --bundle --outfile=$@ --format=esm --minify
NODE_ENV=production NODE_PATH=$(node_path) $(esbuild) $< --log-level=error --bundle --outfile=$@ --format=esm --minify

# Build controller files with shared chunk reference
$(out_dir_js)/%.js: $(src_dir_js)/%.ts $(out_dir_js)/controller.js
NODE_ENV=production $(esbuild) $< --log-level=error --bundle --outdir=$(out_dir_js) --format=esm --define:process.env.NODE_ENV="\"production\"" --minify --external:./controller.js
# Build controller files with shared chunk reference. The bare %.ts
# prerequisite lets `vpath` resolve sources from frontend/js/ OR from
# feature/<name>/frontend/ (set above). $< is the path Make found.
$(out_dir_js)/%.js: %.ts $(out_dir_js)/controller.js
NODE_ENV=production NODE_PATH=$(node_path) $(esbuild) $< --log-level=error --bundle --outdir=$(out_dir_js) --format=esm --define:process.env.NODE_ENV="\"production\"" --minify --external:./controller.js

# Rule to copy static files while preserving directory structure
static: $(output_static)
Expand Down Expand Up @@ -123,7 +146,7 @@ dev.maincss: generate | $(PUBLIC_DIR)

# TS in development mode
dev.ts: $(esbuild) generate | $(PUBLIC_DIR)
NODE_ENV=development $(esbuild) $(input_js) --bundle --outdir=$(out_dir_js) --sourcemap --format=esm --watch \
NODE_ENV=development NODE_PATH=$(node_path) $(esbuild) $(input_js) --bundle --outdir=$(out_dir_js) --sourcemap --format=esm --watch \
2>&1 | $(logname) esbuild

# Install node modules deps located in ./tools
Expand Down
4 changes: 2 additions & 2 deletions gno.land/pkg/gnoweb/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) {
mux.Handle(assetsBase, http.StripPrefix(assetsBase, assetsHandler))

// Handle playground API endpoints
mux.Handle("/_/api/eval", handlerPlaygroundEval(logger, adpcli, cfg.Domain, cfg.NodeRemote))
mux.Handle("/_/api/funcs", handlerPlaygroundFuncs(logger, adpcli))
mux.Handle("/_/api/eval", httphandler.Playground.EvalHandler())
mux.Handle("/_/api/funcs", httphandler.Playground.FuncsHandler())

// Handle status page
mux.Handle("/status.json", handlerStatusJSON(logger, rpcclient))
Expand Down
22 changes: 3 additions & 19 deletions gno.land/pkg/gnoweb/components/view_playground.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
package components

// PlaygroundViewType identifies the playground feature view in
// layout-level switch cases (see layout_index.go). The data type
// and view constructor live in the playground feature package.
const PlaygroundViewType ViewType = "playground-view"

type PlaygroundData struct {
// InitialCode is pre-filled code (e.g. from fork)
InitialCode string
// ForkFrom is the package path this was forked from
ForkFrom string
// Remote is the RPC endpoint
Remote string
// ChainId is the current chain ID
ChainId string
// Domain is the node domain
Domain string
// DefaultFile is the filename that should be focus on first load
DefaultFile string
}

func PlaygroundView(data PlaygroundData) *View {
return NewTemplateView(PlaygroundViewType, "renderPlayground", data)
}
23 changes: 3 additions & 20 deletions gno.land/pkg/gnoweb/components/view_run.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,6 @@
package components

import "path"

// RunViewType identifies the run feature view in layout-level switch
// cases (see layout_index.go). The data type and view constructor live
// in the run feature package.
const RunViewType ViewType = "run-view"

// RunData holds the data for the maketx-run scratchpad view.
type RunData struct {
PkgPath string // full path, e.g. "gno.land/r/demo/boards"
Domain string // e.g. "gno.land"
Remote string // e.g. "https://rpc.gno.land:443"
ChainId string // e.g. "portal-loop"
}

// PkgAlias returns the last segment of the import path, used as the package alias
// in the generated template code (e.g. "boards" from "gno.land/r/demo/boards").
func (d RunData) PkgAlias() string {
return path.Base(d.PkgPath)
}

func RunView(data RunData) *View {
return NewTemplateView(RunViewType, "renderRun", data)
}
30 changes: 30 additions & 0 deletions gno.land/pkg/gnoweb/feature/playground/component.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package playground

import (
"html/template"
"io"

"github.com/gnolang/gno/gno.land/pkg/gnoweb/components"
)

type playgroundComponent struct {
tmpl *template.Template
name string
data any
}

func (c *playgroundComponent) Render(w io.Writer) error {
return c.tmpl.ExecuteTemplate(w, c.name, c.data)
}

// NewPageView wraps the playground template.
func NewPageView(data PlaygroundData) *components.View {
return &components.View{
Type: components.PlaygroundViewType,
Component: &playgroundComponent{
tmpl: PageTemplate,
name: "renderPage",
data: data,
},
}
}
7 changes: 7 additions & 0 deletions gno.land/pkg/gnoweb/feature/playground/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Package playground implements the gnoweb playground feature.
//
// It serves the standalone playground page at /_/play, the fork view
// (?fork on a package or realm URL — concatenates the source files
// into the playground), and the JSON API endpoints used by the
// playground UI (/_/api/eval, /_/api/funcs).
package playground
69 changes: 69 additions & 0 deletions gno.land/pkg/gnoweb/feature/playground/feature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package playground

import (
"context"
"log/slog"

"github.com/gnolang/gno/gnovm/pkg/doc"
)

// ClientAdapter is the subset of the gnoweb chain-client interface that
// the playground feature consumes. Declared locally so feature/playground
// does not import the gnoweb package. The signatures match the
// corresponding methods on gnoweb.ClientAdapter so a *gnoweb.MockClient
// or *gnoweb.rpcClient satisfies this contract through a thin adapter
// wired in at construction time.
type ClientAdapter interface {
// ListFiles is used by the fork view to enumerate package sources.
ListFiles(ctx context.Context, path string) ([]string, error)

// File is used by the fork view to read each source file.
File(ctx context.Context, path, filename string) ([]byte, error)

// Doc is used by the funcs API to enumerate exported functions.
Doc(ctx context.Context, path string) (*doc.JSONDocumentation, error)

// Eval is used by the eval API to run an expression against a
// realm via vm/qeval.
Eval(ctx context.Context, data string) ([]byte, error)
}

// Deps gathers the dependencies the playground Handler needs.
type Deps struct {
Client ClientAdapter

// Logger falls back to slog.Default().
Logger *slog.Logger

// Domain is the chain domain (e.g. "gno.land").
Domain string

// Remote is the RPC endpoint surfaced to the playground UI so it
// can show the user which node it is talking to.
Remote string

// ChainId is the active chain id surfaced to the playground UI.
ChainId string
}

// Handler owns the playground feature state.
type Handler struct {
deps Deps
limiter *rateLimiter
}

// New validates required deps and returns a Handler.
func New(deps Deps) *Handler {
if deps.Client == nil {
panic("playground.New: Client is required")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation is asymmetric here no? Client panics if nil, but empty Domain / Remote / ChainId slide through silently even though they all end up rendered into the page. Not necessarily wrong (maybe empty defaults are legitimate in dev?), just felt inconsistent enough to flag. Either validate them all or drop a one-liner saying empties are intentional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 38a9c78 and also for run feature in d99869d

}

if deps.Logger == nil {
deps.Logger = slog.Default()
}

return &Handler{
deps: deps,
limiter: newRateLimiter(evalBurstSize, evalRefillInterval),
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CodeEditor, isDarkMode } from "./code-editor.js";
import { BaseController, makeCopyIcon } from "./controller.js";
import { CodeEditor, isDarkMode } from "../../../frontend/js/code-editor.js";
import { BaseController, makeCopyIcon } from "../../../frontend/js/controller.js";

interface PlaygroundFile {
name: string;
Expand Down Expand Up @@ -35,8 +35,9 @@ export class PlaygroundController extends BaseController {
this.tabsWrapEl = this.getTarget("tabs-wrap") as HTMLElement;
this.prevBtnEl = this.getTarget("prev-button") as HTMLButtonElement;
this.nextBtnEl = this.getTarget("next-button") as HTMLButtonElement;
if (!this.editorEl || !this.outputEl || !this.tabsEl || !initialCodeEl)
if (!this.editorEl || !this.outputEl || !this.tabsEl || !initialCodeEl) {
return;
}

this.editorEl.addEventListener("focusin", () =>
this._scrollActiveTabIntoView(),
Expand Down Expand Up @@ -170,7 +171,9 @@ export class PlaygroundController extends BaseController {
): void {
const row = document.createElement("div");
row.className = "b-playground-output-item";
if (isError) row.classList.add("u-color-danger");
if (isError) {
row.classList.add("u-color-danger");
}

const pre = document.createElement("pre");
pre.className = "b-playground-output-item-text";
Expand Down Expand Up @@ -210,8 +213,9 @@ export class PlaygroundController extends BaseController {
}

private renderTabs(): void {
while (this.tabsEl.firstChild)
while (this.tabsEl.firstChild) {
this.tabsEl.removeChild(this.tabsEl.firstChild);
}

this.files.forEach((f, i) => {
const btn = document.createElement("button");
Expand All @@ -227,7 +231,9 @@ export class PlaygroundController extends BaseController {

public switchTab(event: Event & { params?: Record<string, unknown> }): void {
const fileName = event.params?.file as string;
if (fileName) this._switchToFile(fileName);
if (fileName) {
this._switchToFile(fileName);
}
}

public addFile(): void {
Expand Down
Loading
Loading