Skip to main content

react-webapp

How to build a React SPA on the platform — project layout, the build-verify command, and this stack's constraints and pitfalls. Apply when a component's `type` is `web-application`.

ソース情報

リポジトリ
wso2/labs-agentic-engineer
ソースの最終更新活動
2026年10月7日 07:08
検出された SKILL.md の言語
英語
スター
25
フォーク
24

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

ファイルエクスプローラー
13 ファイル

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
react-webapp
description
How to build a React SPA on the platform — project layout, the build-verify command, and this stack's constraints and pitfalls. Apply when a component's `type` is `web-application`.
metadata
{"aep":{"kind":"org","audience":["coding"]}}
# React Webapp A web-app on this platform: a Vite + TS SPA built to static files, served by stock `nginx:alpine`. The image is **byte-identical across every environment**. Per-env values the **browser** needs (OIDC config, flags) arrive at request time in `window._env_`, never at build time. Sibling API addresses are **not** browser config — they are pod env for nginx. ## Development flow 1. **Scaffold** per Layout, including the nginx drop-in copy in step 1 of Layout. Read [mock-mode.md](references/mock-mode.md) and include its dependencies in `package.json`, then install **once** with the complete dependency set — npm can only satisfy a peer set it sees all at once, and a package added to an already-resolved tree costs a second full resolve at best. With a design system, its own Setup step IS that install (it writes the manifest and runs `npm install` itself); without one, `npm install`. 2. **Prepare shared interfaces** — write `src/env.ts`, generate `src/generated/` from each dependency's OpenAPI contract, and write `src/api.ts` with a **same-origin** `baseUrl`. With auth, copy `thunder-authentication`'s `assets/app/` tree (one `cp -r`, which lays down `src/authz/` and `mock/authz/`), set the `USER_AUTH_` prefix in `src/authz/session.ts` (mock mode substitutes that module), and run `npm run gen` once so the three generated tables exist for the type-check. 3. **Implement pages** — follow Constraints, and check `src/api.ts` against the **first** page with `npx tsc --noEmit` before writing the rest: that pair proves how the generated client types, and every later page repeats the pattern. 4. **Mock mode** — author `mock/` per `references/mock-mode.md`, including the four wiring files in its §2. It stands the same app up with no cluster, no sibling service and no IDP behind it — with the API gateway's own refusals modelled from `openapi.yaml`, so a role that cannot reach a screen fails here the way it fails in a cell. The build eliminates all of it as dead code. 5. **Verify** — from the app path: ```bash npm install # regenerates package-lock.json # ← the design system's check goes here (see below) npm run gen # with auth: so the standalone tsc below reads a # fresh table (`npm run build` re-runs it) npx tsc --noEmit # type-check without emitting npm run build # actually build ! grep -rq mockServiceWorker dist/ # the bundle carries no mock — step 4 git status --porcelain --ignored=matching -- . \ | grep '^!!' | grep -vE 'node_modules|dist' # ← output MUST be empty ``` Commit the `package-lock.json` this produces. Never commit `node_modules/`. **The last line is not a formality.** Every step above it reads your working tree; the cluster builds the *committed* tree of this folder alone. A build input that git ignores is present for all four checks and absent from the image, and git will not tell you: `git add <app-path>` skips an ignored file **silently, exit 0**, and leaves `git status` clean. `src/generated/` is the one that bites, because the repo-root `.gitignore` is shared with backend components that legitimately ignore a `generated/` directory — an unanchored pattern there reaches down into this app. `--ignored=matching` is what makes those paths visible; `node_modules` and `dist` are the only two the builder stage makes for itself, which is why they are the only two filtered out. A `!!` line naming anything else means the image will not carry that file. Fix the pattern (anchor it in the repo-root `.gitignore`), never `git add -f`. **The design-system skill contributes one step to this sequence**, and it is mandatory: run the command its own Verify section names, after `npm install` and before the type-check, and treat a non-zero exit exactly like a failing build. That slot exists because a design system's own wiring — a missing build plugin, an unimported theme, a peer-dependency mismatch — is the one class of fault `tsc` and `vite build` cannot see: it type-checks and builds perfectly clean, then renders an unstyled page in the cluster. If the pinned design-system skill names no such command, the sequence is just the five above. The `build` script is `tsc --noEmit && vite build` — **not** `tsc -b`, which needs a composite project: a `tsconfig.json` that `references` a `tsconfig.node.json` setting `noEmit` fails with `TS6310: Referenced project may not disable emit`, and unwinding that costs more than it buys. **With an auth dependency `build` gains a `gen` step in front:** `npm run gen && tsc --noEmit && vite build`, where `gen` regenerates the authorization tables (`src/authz/roles.gen.ts`, `src/authz/operations.gen.ts`, `mock/authz/roles.gen.ts`) from the permission catalog and the contracts. It is not optional and it must come BEFORE the type-check — a stale generated union type-checks perfectly green while the app gates on operations the design no longer has (`thunder-authentication`). Verification ends at exit 0. **Never run `npm audit` or `npm audit fix`** — the advisories land on Vite's dev-only transitive dependencies, which never reach a static bundle served by nginx, and `audit fix` bumps pinned dependencies behind your back. 5. **Walk** — `mock-verification`, another agent's dispatch. Your job ends at a clean Verify with `mock/` in place. 6. **PR** — the lead's, once the walk has reported; an open `[ ]` line rides in its body (the component contract's **Walks**). ## Constraints **Runtime config, not build-time.** The platform mounts `/env-config.js` into the served root and it populates `window._env_`. You never generate or commit that file. `import.meta.env.VITE_*`, `process.env.REACT_APP_*`, `NEXT_PUBLIC_*` and `.env` files are all build-time mechanisms the platform does not use — reading one gets you `undefined` in production. **The key set is fixed.** It is hardcoded in platform code, so a key you invent is `undefined` at module load. Use these exact spellings: | Key | Set when | Meaning | |---|---|---| | `<NAME>_URL` | `dependencies` include an `external`-kind entry `<name>` | URL of that **external** upstream (browser may call it). Not used for a sibling `component`-kind service. | | `<DEP>_*` | this web-app declares an auth `platform-resource` dependency named `<dep>` | OIDC config — `<DEP>_CLIENT_ID`, `<DEP>_ISSUER`, `<DEP>_JWKS_URL`, `<DEP>_SCOPES` and **`<DEP>_RESOURCE`** (the project's resource-server identifier, the RFC 8707 `resource` indicator the SPA must send or every `/api` call 401s). `<DEP>` = UPPER_SNAKE of the dependency name (`user-auth` → `USER_AUTH_*`) — owned by `thunder-authentication` | | `<NAME>` (any) | you declared it in `workload.yaml` `configurations.env` | app-config default, per-env override possible | There is **no** `API_BASE_URL` and **no** `<UPSTREAM>_URL` in `window._env_` for a sibling service. The sibling lives at **same-origin** `/api` (extra siblings: `/api/<component-name>/`). Its addresses — `<DEP>_GATEWAY_URL` and `<DEP>_URL` — are **pod** env vars, never browser keys; only the nginx drop-in reads them (see **Same-origin API proxy** below). **Throw on a missing key, never default it.** No `?? ""`, no `|| ''`, for keys this table says are set. A silent fallback hides a missing OIDC issuer. Do not declare sibling API URL keys on `Env` just to throw — they are not emitted. **Served at host root.** Each web-app gets its **own** gateway hostname, so the stock Vite default is correct: **do NOT set `base`**. Asset URLs, any react-router `basename`, and any OAuth `redirect_uri` are plain root paths (`/assets/…`, `/callback`). Services ARE path-routed, under `/<project>-<component>-http` on a shared gateway — copying that prefix into `base` 404s every asset. **Same-origin API proxy, through the gateway.** Nginx reverse-proxies `location /api/` to the primary sibling. Two pod env vars address that sibling and they are NOT interchangeable: | Pod env var | Reaches | Auth | |---|---|---| | `<DEP>_GATEWAY_URL` | the API gateway's **runtime Service**, `…-gateway-runtime.<ns>.svc.cluster.local:22893` — a cluster address, never the public vhost. Set by the platform for a sibling whose design declares `exposesAPI.auth`. Carries a **context path prefix**. | validates the bearer token, injects `X-User-*` from its claims | | `<DEP>_URL` | the project Service, directly | none — nothing validates a token, nothing injects identity | The asset **prefers `<DEP>_GATEWAY_URL`** and falls back to `<DEP>_URL`. That order is the whole point: browser traffic is untrusted, and this proxy is the one hop that would otherwise carry it into the project's trusted lane with no authentication in between. Two rules follow, and the asset already obeys both — which is why you copy it rather than write it: - **Preserve the context prefix.** The gateway routes on it; a rewrite that strips it 404s every call. - **Clear inbound identity.** Identity is the gateway's to assert (`api-management`), and nothing a browser sends on this lane may stand in for it. The asset clears six headers: **`x-jwt-assertion`** — the gateway-signed statement the service actually believes, which on a public operation the gateway overwrites with nothing — and the five unsigned `X-User-*`. Copy the assets in Layout; do not hand-write a different `proxy_pass`, do not add `/oidc/` (token endpoint stays cross-origin; `thunder-authentication`), do not copy `apps/console/docker-entrypoint.sh`. Keep the official `nginx:alpine` `ENTRYPOINT`. The image adds exactly the three asset files: `nginx.conf`, the `default.conf` template and `/docker-entrypoint.d/15-aep-api-proxy.sh`. **Auth.** If the component declares an auth `platform-resource` dependency, its sign-in, screen gating and API-client wiring are `thunder-authentication`'s: copy that skill's `assets/app/` tree as one (`src/authz/`, `mock/authz/`, `scripts/gen-authz.mjs`), and reach the API through `apiFetch`/`apiJson` rather than attaching a bearer by hand. A screen is gated on the **operation it loads**, named once in `src/authz/screens.ts` and checked against the generated `src/authz/operations.gen.ts`; nothing about screens is in `security.json` (`authorization-model`). **Never `exposesAPI`.** That toggle is for backends only; a web-app expresses auth through its auth dependency instead. **The UI comes from the organization's design system.** Every component, layout primitive and style under `src/` comes from the design-system skill pinned on this component — no raw HTML styling, no second component or styling library. The one carve-out is `thunder-authentication`'s copied assets: `src/authz/gates.tsx`'s `Forbidden` and `NoAccess` ship as plain `<main>/<section>/<h1>` so the file is copied verbatim into any app, whatever design system it pins. Treat that markup as structure to restyle with the design system's components after copying, and keep the words — they are prescribed. That skill owns everything inside `src/`; this skill owns the app around it. Where the two appear to disagree — `base`, the `index.html` script tags, nginx, `window._env_` — **this skill wins**, because those are deployment facts, not style preferences. The data layer is untouched either way: `openapi-fetch` and the committed `src/generated/` client stay exactly as specified above. **Contract-first client, never hand-rolled shapes.** Every dependency has a committed OpenAPI contract: `specs/design/components/<component-name>/openapi.yaml` for a `component`-kind dependency, or `specs/design/components/<this-app>/dependencies/<dep-name>.openapi.yaml` for an `external`-kind one — project-root paths, sibling to this app's own folder. Generate types from it and call through `openapi-fetch`'s typed client (Layout); don't hand-write request/response shapes. Commit `src/generated/` — the per-component Docker build's context is this app's own folder alone. **An `ai-agent` dependency has no OpenAPI contract — it has one fixed chat contract.** It is a sibling like any other: reached **same-origin**, never through a `window._env_` URL — an agent has no browser-visible `<AGENT>_URL` any more than an API sibling does. If it is the primary dependency, call it at `/api`; if it is an extra sibling, call it at `/api/<agent-component-name>/` (Layout's nginx section covers wiring either into the drop-in). It is called with the same bearer token as any sibling service, but there is nothing to generate from; hand-write this one small client against the shape every platform agent speaks: ```ts // POST /api/chat (or /api/<agent-component-name>/chat for an extra sibling) // in: { conversationId?: string, message: string, // attachments?: [{ name: string, mediaType: string, data: string /* base64 */ }] } // out: { conversationId: string, text: string, toolCalls: unknown[] } ``` The agent keeps the conversation. Your state is exactly two things: - **`conversationId`** — from the first response; send it on every later message. "New conversation" = drop it. Persist it in `sessionStorage` if the conversation should survive a refresh. - **Your own transcript** — `{ role: "user" | "assistant", text: string }[]`, built as you go: push the user's text when they send, push `response.text` when the agent replies. That list is for RENDERING and is yours alone; the agent never sees it and never returns one. No `messages` array crosses the wire in either direction. **Files, only when the agent takes them.** If the agent's `specs/design/components/<agent>/agent.afm.md` declares `x-aep.attachments`, give the chat an attach control and compile its `types`, `maxFiles` and `maxFileSizeMB` in as constants: the picker's `accept` is the types; count, per-file size and the 15 MB total are checked before sending, naming the file that fails. Send each file base64 in `attachments`; `message` may be empty when files are present. Without the block, render no attach control. The agent keeps no files, so on a 400, 413 or 422 keep the files and their text in the composer for a retry and show the agent's `error`. A `404` from `/chat` means the conversation is gone or was never yours — drop the stored id, start fresh, and tell the user the previous conversation expired. Do not retry the same id. A `422` whose body carries `guardrail` (`{ error, guardrail }`) means the platform's AI gateway refused that message on policy — the agent is up. Say so in the user's terms with the agent's `error` as the reason ("This message was blocked by a safety check: <error>"), keep their text in the composer to edit, and keep the conversation as it was: the blocked turn left nothing behind. Reserve "couldn't reach the assistant" for a network failure or a 5xx. ## Layout ``` <app-path>/ ├── package.json ├── tsconfig.json # ONE file — no project references, no tsconfig.node.json ├── vite.config.ts # no `base` — served at host root ├── index.html ├── scripts/ │ └── gen-authz.mjs # only with an auth dependency — copied verbatim, run by `npm run gen` ├── src/ │ ├── main.tsx │ ├── App.tsx │ ├── env.ts # typed window._env_ shim │ ├── generated/ # openapi-typescript output, one file per dependency — commit, never hand-edit │ ├── api.ts # openapi-fetch client(s), typed against generated/ │ ├── authz/ # ┐ only with an auth dependency — thunder-authentication owns │ │ ├── session.ts # │ the whole directory. session/core/gates/client arrive │ │ ├── core.ts # │ verbatim with one `cp -r`; screens.ts is a pattern you │ │ ├── gates.tsx # │ adapt; the two *.gen.ts are GENERATED and COMMITTED — │ │ ├── client.ts # │ the per-component build context cannot see ../specs, │ │ ├── screens.ts # │ so an uncommitted one fails the image build │ │ ├── roles.gen.ts # │ │ │ └── operations.gen.ts # ┘ │ ├── shell/AppShell.tsx # the app chrome — every gated screen renders inside it │ └── pages/ # design-system components only, never raw HTML ├── mock/ # mock mode — references/mock-mode.md (mock/authz/ comes with the tree above) ├── nginx/ │ ├── nginx.conf # copied from the skill assets, unchanged │ ├── default.conf # copied from the skill assets, then /api locations kept │ └── 15-aep-api-proxy.sh ├── Dockerfile ├── .dockerignore # what `COPY . .` leaves behind └── .gitignore # `node_modules` and `dist` — both are made by a build # and neither belongs in the project repo ``` **Copy the nginx assets first, and never run a project generator** — `npm create vite` and friends emit a different shape (project-referenced tsconfigs, starter CSS, sample SVGs) and all of it has to be undone before Verify passes. The tree above IS the shape. From the App Path: ```bash mkdir -p nginx cp "$AEP_SKILLS_DIR/react-webapp/assets/nginx.conf" nginx/nginx.conf cp "$AEP_SKILLS_DIR/react-webapp/assets/nginx-default.conf" nginx/default.conf cp "$AEP_SKILLS_DIR/react-webapp/assets/15-aep-api-proxy.sh" nginx/15-aep-api-proxy.sh ```
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る