diff --git a/cmd/add/solution_playground.go b/cmd/add/solution_playground.go index cf3ee5fb..c67dbc78 100644 --- a/cmd/add/solution_playground.go +++ b/cmd/add/solution_playground.go @@ -191,7 +191,7 @@ FRONTEND=${endpoint:%s/web/http} // playgroundMakefile writes the run line, whose one non-obvious part is that // authority/edge is NAMED. Nothing starts it otherwise. func playgroundMakefile(name string) string { - return fmt.Sprintf(`.PHONY: playground + return fmt.Sprintf(`.PHONY: playground remote # %s on the development authority, as a solution runs in a cell with no saas # host: the authority's edge is the one origin the browser opens and signs a @@ -204,8 +204,19 @@ func playgroundMakefile(name string) string { # origin that can sign anyone in. PLAYGROUND_SERVICES = authority/edge %s/web %s/backend -playground: +# THE REMOTE IS BUILT FIRST, and that is not a convenience. The solution +# runtime serves /assets/ from ASSETS_DIR, which defaults to +# ../fe-remote/dist relative to the backend — so with no build there, the +# backend answers 404 for /assets/mf-manifest.json, the manifest it +# advertises in /.well-known/solution.json, and a host can federate nothing. +# The shell also installs the remote as a copy, so it needs it built. +playground: remote cd %s && codefly run service $(PLAYGROUND_SERVICES) --env local + +.PHONY: remote +remote: + cd services/fe-remote && npm install --no-audit --no-fund && npm run build + test -f services/fe-remote/dist/mf-manifest.json `, titleFromName(name), name, name, playgroundDir) } diff --git a/cmd/add/solution_test.go b/cmd/add/solution_test.go index 48df770d..903fd5a4 100644 --- a/cmd/add/solution_test.go +++ b/cmd/add/solution_test.go @@ -555,3 +555,25 @@ func TestTheContractDeclaresARoleBesidesInstaller(t *testing.T) { t.Fatalf("the contract must declare installer and viewer, got %v", names) } } + +// TestThePlaygroundBuildsTheRemoteFirst. The solution runtime serves /assets/ +// from ASSETS_DIR, which defaults to ../fe-remote/dist relative to the +// backend. With nothing built there the backend answers 404 for +// /assets/mf-manifest.json — the manifest it advertises in +// /.well-known/solution.json — and a host can federate nothing. The fleet's +// own playground builds the remote before the run for exactly this reason. +func TestThePlaygroundBuildsTheRemoteFirst(t *testing.T) { + makefile := playgroundMakefile("notes") + if !strings.Contains(makefile, "playground: remote") { + t.Fatalf("the playground must build the remote first:\n%s", makefile) + } + for _, required := range []string{ + "cd services/fe-remote && npm install", + "npm run build", + "test -f services/fe-remote/dist/mf-manifest.json", + } { + if !strings.Contains(makefile, required) { + t.Fatalf("the remote target must contain %q", required) + } + } +} diff --git a/docs/commands.md b/docs/commands.md index dde8b037..851e1047 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -2435,8 +2435,9 @@ What it creates: `services/fe-remote` — the `./Page` remote itself — is written by the solution agent, not by this command: it is an npm package, not a Codefly service. -**Running it.** `make playground` starts the development authority and this -solution, and prints the one address a browser opens: +**Running it.** `make playground` builds the page's remote, starts the +development authority and this solution, and prints the one address a browser +opens: ``` edge listening on :14581, routing 1 module(s) @@ -2450,6 +2451,12 @@ not behind its sign-in edge") and the authority's REST port (every browser path 404s). In a cell the host's gateway plays the edge's part; the playground is local only. +The remote is built **before** the run, and that is load-bearing: the solution +runtime serves `/assets/` from `ASSETS_DIR`, which defaults to +`../fe-remote/dist` relative to the backend. With nothing built there the +backend answers 404 for `/assets/mf-manifest.json` — the manifest it +advertises in `/.well-known/solution.json` — and a host can federate nothing. + Sign in as any of the fixture's acme people in a team — `maya`, `remi`, `alex`, `dev` — and the solution's route answers: