Move reference plugin to examples/scheduling-plugin; plugins/ ships empty as a drop-in mount point, e2e bind-mounts the example
This commit is contained in:
@@ -5,3 +5,7 @@ node_modules
|
||||
|
||||
# Playwright E2E outputs (screenshots, html report, traces)
|
||||
e2e-tests/artifacts/
|
||||
|
||||
# plugins/ is a drop-in mount point, not committed code — keep it empty (see examples/ for the reference)
|
||||
/plugins/*
|
||||
!/plugins/.gitkeep
|
||||
|
||||
@@ -53,7 +53,7 @@ is the plugin id *and* the mount path) and "Hello" is in the menu. That's the wh
|
||||
|
||||
From here, render real pages against the app shell and fetch upstream data — see
|
||||
[Building plugins](#building-plugins) and the runnable reference in
|
||||
[`plugins/scheduling/`](plugins/scheduling/).
|
||||
[`examples/scheduling-plugin/`](examples/scheduling-plugin/).
|
||||
|
||||
## Contents
|
||||
|
||||
@@ -205,11 +205,15 @@ override (see [The menu system](#the-menu-system)).
|
||||
This is the **authoritative reference** for the plugin API — the product's main surface. The
|
||||
contract is **TypeScript** (`src/plugin-host/plugin.ts`), so the types there are the single
|
||||
source of truth; the sections below explain them, the guarantees around them, and the rules
|
||||
the host enforces. A complete, runnable example ships in
|
||||
**[`plugins/scheduling/`](plugins/scheduling/)** — a public overview page, a permission-gated
|
||||
list page fetching upstream data (it points `SCHEDULING_UPSTREAM` at its backend; the dev
|
||||
compose ships a tiny mock, `examples/shifts-upstream/`), a CSRF-guarded form forwarding writes
|
||||
upstream, and a mix of public + role-gated nav. Copy it and adapt.
|
||||
the host enforces. A complete, runnable example lives in
|
||||
**[`examples/scheduling-plugin/`](examples/scheduling-plugin/)** — a public overview page, a
|
||||
permission-gated list page fetching upstream data (it points `SCHEDULING_UPSTREAM` at its backend;
|
||||
the dev compose ships a tiny mock, `examples/shifts-upstream/`), a CSRF-guarded form forwarding
|
||||
writes upstream, and a mix of public + role-gated nav. It is **not** pre-installed — `plugins/`
|
||||
ships empty so you mount your own; the E2E suites bind-mount this example onto
|
||||
`/app/plugins/scheduling` to exercise it. To run it in dev, copy it in
|
||||
(`cp -r examples/scheduling-plugin plugins/scheduling`, then restart) — the dev compose already
|
||||
points `SCHEDULING_UPSTREAM` at its mock backend. Copy it to `plugins/<id>/` and adapt.
|
||||
|
||||
**Design stance.** The audience is experienced developers. The API optimises for being
|
||||
**powerful, predictable, and overloadable** — a plugin can take over as much of a page as it
|
||||
@@ -439,11 +443,11 @@ role-filtered, and current-marked for this request (the gated **Dashboard** link
|
||||
anonymous visitor). `chrome.signInHref` is where the shell's anonymous **Sign in** link points — the
|
||||
current page baked in as `return_to`. Map each `chrome.*` to the matching `partials/shell` local —
|
||||
`brand`, `csrfToken`, `nav` (the rendered nav-tree), `signInHref`, `theme`, `user` — exactly as the
|
||||
reference `plugins/scheduling/views/overview.ejs` does; a value you forget simply falls back to its
|
||||
reference `examples/scheduling-plugin/views/overview.ejs` does; a value you forget simply falls back to its
|
||||
shell default (e.g. a bare `/login`), it does not error. **`ctx.verifyCsrf(submitted)`** guards a
|
||||
state-changing form: render `chrome.csrfToken` in a hidden `_csrf` field, then on POST read your own
|
||||
body and `if (!ctx.verifyCsrf(form.get("_csrf"))) throw new GuardError(403, …)`. The host owns the
|
||||
secret and sets the cookie; the plugin never touches it. (See the reference: `plugins/scheduling/`.)
|
||||
secret and sets the cookie; the plugin never touches it. (See the reference: `examples/scheduling-plugin/`.)
|
||||
|
||||
The same shell renders **every** page (the dashboard, the admin screens, your plugin pages, and the
|
||||
login/registration/front pages), so the menu looks identical signed in or out — it just role-filters.
|
||||
@@ -617,7 +621,7 @@ reproducible; mount a volume only to add plugins to an already-built image.
|
||||
### Local dev & test story
|
||||
|
||||
A plugin is a normal folder of TypeScript, so an author tests it the same way the core is tested
|
||||
— everything in Docker, no host tooling. The shipped reference (`plugins/scheduling/`) is the
|
||||
— everything in Docker, no host tooling. The reference example (`examples/scheduling-plugin/`) is the
|
||||
worked example: thin handlers bound to an injectable upstream client, unit-tested in
|
||||
`shifts.test.ts` with a mocked `fetch` and a hand-built `ctx` (no host).
|
||||
|
||||
@@ -1276,8 +1280,8 @@ views/ Core EJS templates, all in the one app shell: home (public
|
||||
public/ Static assets under /public/ (css/styles.css + auth.css, favicon, robots.txt)
|
||||
config/menu.ts Central menu override + branding (optional; defaults apply if absent)
|
||||
ory/ Ory service config (kratos/: identity schema, kratos.yml, oidc/ SSO claims mapper, tokenizer/ session→JWT claims mapper + dev signing JWKS; keto/: keto.yml + namespaces.keto.ts OPL — role/group/resource; hydra/hydra.yml: OAuth2 issuer + login/consent URLs → /oauth2/*) + storage init (postgres/init/init.sql: one DB per service)
|
||||
plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in). Ships scheduling/ — the reference plugin (list/form over an upstream + permission-gated nav) you copy
|
||||
examples/ Non-app helpers; shifts-upstream/ is the dev mock backend the reference plugin reads/writes (stand-in for your real service)
|
||||
plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in). Ships empty (.gitkeep, git-ignored otherwise) — mount your own; the E2E suites bind-mount the reference example onto /app/plugins/scheduling
|
||||
examples/ Non-app helpers: scheduling-plugin/ is the reference plugin (list/form over an upstream + permission-gated nav) you copy into plugins/; shifts-upstream/ is the dev mock backend it reads/writes (stand-in for your real service)
|
||||
e2e-tests/ Playwright E2E: visual.spec (design system, Ory-free) + auth-refresh.spec (token timeout/re-mint) + oauth-login.spec (OAuth2 login + consent) + full-flow.spec (browser UI: password/SSO login, menu-by-role, admin CRUD, plugin page, logout) + devstack-login.spec (regression: login works from the banner's localhost URL and 127.0.0.1 is canonicalised, on the plain `docker compose up` topology); proxy.ts (same-origin gateway) + mock-oidc.ts (mock SSO provider) back full-flow. e2e-tests/Dockerfile + e2e-tests/compose.{visual,auth,oauth,full,devstack}.yml run them
|
||||
ci.sh The full CI gate: typecheck → unit tests → every E2E suite, each on a fresh, always-torn-down stack (`bash ci.sh`)
|
||||
```
|
||||
|
||||
@@ -15,14 +15,15 @@ services:
|
||||
LOG_LEVEL: "debug" # verbose by default while developing (base defaults to info)
|
||||
REQUIRE_SECURE_SECRETS: "false"
|
||||
SECURE_COOKIES: "false" # dev serves http — Secure cookies wouldn't be sent
|
||||
SCHEDULING_UPSTREAM: "http://shifts-upstream:4000" # reference plugin → the dev mock backend
|
||||
SCHEDULING_UPSTREAM: "http://shifts-upstream:4000" # backs the reference plugin once you copy it into plugins/
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
|
||||
# Dev mock backend for the reference plugin (plugins/scheduling). A stand-in for the customer's
|
||||
# real scheduling service — stdlib-only, in-memory, no auth. Prod points SCHEDULING_UPSTREAM at
|
||||
# the real backend instead. Uses the pinned app image so there's nothing extra to build/pull.
|
||||
# Mock backend ready for the reference plugin (examples/scheduling-plugin): plugins/ ships empty, so
|
||||
# the plugin is opt-in — `cp -r examples/scheduling-plugin plugins/scheduling`, restart, and this
|
||||
# backs it (SCHEDULING_UPSTREAM above points here). Stand-in for the customer's real service —
|
||||
# stdlib-only, in-memory, no auth. Prod points SCHEDULING_UPSTREAM at the real backend instead.
|
||||
shifts-upstream:
|
||||
image: node:24.16.0-alpine3.24
|
||||
command: node /srv/server.ts
|
||||
|
||||
@@ -28,6 +28,16 @@ services:
|
||||
interval: 2s
|
||||
timeout: 4s
|
||||
retries: 30
|
||||
# plugins/ is empty in the image; bind the reference example in as the `scheduling` plugin so the
|
||||
# browser flow can open its gated /scheduling/shifts page.
|
||||
volumes:
|
||||
- ./examples/scheduling-plugin:/app/plugins/scheduling:ro
|
||||
|
||||
# bootstrap grants the demo admin every discovered plugin's permission tokens, so it needs the
|
||||
# reference plugin present too — else the admin lacks scheduling:read/write and the gated page 403s.
|
||||
bootstrap:
|
||||
volumes:
|
||||
- ./examples/scheduling-plugin:/app/plugins/scheduling:ro
|
||||
|
||||
# Browser-facing URLs (base_url, every ui_url, the after-login redirect) move to the gateway host.
|
||||
# `--dev`: the browser hits the gateway over http, but Kratos marks cookies Secure for a
|
||||
|
||||
@@ -20,6 +20,10 @@ services:
|
||||
interval: 2s
|
||||
timeout: 4s
|
||||
retries: 15
|
||||
# plugins/ is empty in the image; bind the reference example in as the `scheduling` plugin so the
|
||||
# nav-gating spec has a drop-in plugin to assert against.
|
||||
volumes:
|
||||
- ./examples/scheduling-plugin:/app/plugins/scheduling:ro
|
||||
|
||||
e2e:
|
||||
build:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Dev-only mock upstream for the reference plugin (plugins/scheduling) — a stand-in for the
|
||||
// customer's real backend so `docker compose up` shows the plugin working out of the box. NOT part
|
||||
// Dev-only mock upstream for the reference plugin (examples/scheduling-plugin) — a stand-in for the
|
||||
// customer's real backend, ready for when you copy the reference plugin into plugins/. NOT part
|
||||
// of the app: stdlib only, in-memory (state resets on restart), no auth. Point SCHEDULING_UPSTREAM
|
||||
// at your real service in production.
|
||||
//
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"dev": "node --watch src/server.ts",
|
||||
"gen-jwks": "node src/auth/gen-jwks.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "node --test \"src/**/*.test.ts\" \"plugins/**/*.test.ts\""
|
||||
"test": "node --test \"src/**/*.test.ts\" \"plugins/**/*.test.ts\" \"examples/**/*.test.ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@larvit/log": "2.3.0",
|
||||
|
||||
@@ -2,6 +2,6 @@ For each todo item, interview the user extensively to deeply understand the scop
|
||||
|
||||
- [x] Organize the files in src in to folders so it is easier to understand the structure of the code.
|
||||
- [x] Move docs/plugin-contract.md into README.md and remove the docs folder.
|
||||
- [ ] The plugins/scheduling is an example and shouldn't be committed to the plugins directory since that should be empty to be able to be mounted in via docker or other means for the users/develoeprs using this application/framework. Put it in the examples folder instead.
|
||||
- [ ] Build and publish docker image as CI/CD. Ma
|
||||
- [x] The plugins/scheduling is an example and shouldn't be committed to the plugins directory since that should be empty to be able to be mounted in via docker or other means for the users/develoeprs using this application/framework. Put it in the examples folder instead.
|
||||
- [ ] Build and publish docker image as CI/CD.
|
||||
- [ ] Add i18n support.
|
||||
+1
-1
@@ -24,5 +24,5 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["config", "plugins", "src"]
|
||||
"include": ["config", "examples/scheduling-plugin", "plugins", "src"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user