|
| 1 | +# Portable Functions Toolkit |
| 2 | + |
| 3 | +The Constructive Functions toolkit lets any external repo `pnpm add` a small set of npm packages, drop in its own `functions/` directory, and get a full code-gen + Docker + k8s + manifest-registry pipeline — without git submodules or copy-paste. |
| 4 | + |
| 5 | +## Package layout (Starship V2 style) |
| 6 | + |
| 7 | +``` |
| 8 | +fn-cli ──► fn-client ──► fn-generator ──► fn-types |
| 9 | + └────────────────────► fn-types |
| 10 | + └────────► fn-types |
| 11 | +fn-runtime ──► fn-types (handlers import this + fn-types) |
| 12 | +knative-job-fn (fn-app) (low-level Express middleware) |
| 13 | +``` |
| 14 | + |
| 15 | +| Package | Single responsibility | |
| 16 | +|---|---| |
| 17 | +| `@constructive-io/fn-types` | Source-of-truth TS types: `FunctionHandler`, `FunctionContext`, `HandlerManifest`, `FnRegistry`, `FnConfig` + `defineConfig()`. No logic. | |
| 18 | +| `@constructive-io/fn-runtime` | Express server factory + GraphQL clients + log + job-callback wiring. The contract handlers import. | |
| 19 | +| `@constructive-io/knative-job-fn` | Low-level Express middleware for Knative job request/response shape. fn-runtime depends on it. | |
| 20 | +| `@constructive-io/fn-generator` | Programmatic builders that emit Dockerfiles, k8s YAML, configmaps, skaffold profiles, manifest registry. Pure functions; idempotent file I/O at the boundary. | |
| 21 | +| `@constructive-io/fn-client` | Importable `FnClient` API — config loading, manifest reading, `pnpm build`, child-process orchestration for `dev`. | |
| 22 | +| `@constructive-io/fn-cli` | The `fn` executable. Subcommands: `init`, `generate`, `build`, `dev`, `manifest`, `verify`. | |
| 23 | + |
| 24 | +## Quick start |
| 25 | + |
| 26 | +```bash |
| 27 | +# In a fresh project |
| 28 | +pnpm add -D @constructive-io/fn-cli |
| 29 | +pnpm add @constructive-io/fn-runtime |
| 30 | + |
| 31 | +# Scaffold a function |
| 32 | +pnpm fn init send-welcome --no-tty --description "Welcome email sender" |
| 33 | +# → functions/send-welcome/{handler.json, handler.ts} |
| 34 | + |
| 35 | +# Stamp out the workspace package, build, run |
| 36 | +pnpm fn generate |
| 37 | +pnpm install # link the just-created generated/* workspaces |
| 38 | +pnpm fn build |
| 39 | +pnpm fn dev # functions run as local Node processes |
| 40 | +``` |
| 41 | + |
| 42 | +`fn init` uses [`genomic`](https://www.npmjs.com/package/genomic) under the hood — the same template engine `pgpm init` uses — so the prompt conventions and `--no-tty` flag-mapping match the rest of the Constructive ecosystem. Two handler types ship today: `--type=node-graphql` (default) and `--type=python`. |
| 43 | + |
| 44 | +## Repo layout the toolkit expects |
| 45 | + |
| 46 | +``` |
| 47 | +my-app/ |
| 48 | +├── functions/ |
| 49 | +│ └── send-welcome/ |
| 50 | +│ ├── handler.json # {"name":"send-welcome","version":"0.1.0","type":"node-graphql"} |
| 51 | +│ └── handler.ts # default-exported FunctionHandler |
| 52 | +├── fn.config.json # FnConfig (typed via fn-types) — optional |
| 53 | +└── package.json |
| 54 | +``` |
| 55 | + |
| 56 | +## CLI surface |
| 57 | + |
| 58 | +```bash |
| 59 | +fn init <name> [--type=node-graphql|python] [--description=<d>] [--force] [--no-tty] |
| 60 | +fn generate [--only=<name>] [--packages-only] |
| 61 | +fn build [--only=<name>] |
| 62 | +fn dev [--only=<name>] |
| 63 | +fn manifest # print on-disk functions-manifest.json |
| 64 | +fn verify # check manifest matches functions/ |
| 65 | +fn --version # print fn-cli version |
| 66 | +``` |
| 67 | + |
| 68 | +Common flags: `--root=<dir>`, `--config=<file>`. |
| 69 | + |
| 70 | +## Job-service registry (when running the `jobs-bundle` preset) |
| 71 | + |
| 72 | +The job-service no longer hardcodes function names. It loads its registry at startup from one of three sources, in priority order: |
| 73 | + |
| 74 | +1. `FUNCTIONS_REGISTRY` env var |
| 75 | + Format: `name:moduleName:port,...` — `moduleName` and `port` are optional (missing `moduleName` falls back to `@constructive-io/<name>-fn`). |
| 76 | + |
| 77 | +2. `FUNCTIONS_MANIFEST_PATH` env var pointing to a JSON file with the existing `functions-manifest.json` shape. Manifest entries can carry an optional `moduleName` field; otherwise convention applies. |
| 78 | + |
| 79 | +3. Default file: `<cwd>/generated/functions-manifest.json` — what `fn generate` produces. |
| 80 | + |
| 81 | +Empty registry is allowed; lookups still throw `Unknown function "<name>"` to preserve the legacy behaviour. |
| 82 | + |
| 83 | +## Releasing the toolkit (Wave 5) |
| 84 | + |
| 85 | +The CI workflow at `.github/workflows/publish.yaml` publishes all six packages with [npm provenance](https://docs.npmjs.com/generating-provenance-statements) when a `fn-v*` tag is pushed. Steps: |
| 86 | + |
| 87 | +1. Update versions in each `packages/fn-*/package.json` (and `packages/fn-app/package.json`). Bump in lock-step for now; we'll move to changesets later. |
| 88 | +2. Verify locally: |
| 89 | + ```bash |
| 90 | + pnpm --filter '@constructive-io/fn-*' build |
| 91 | + pnpm --filter @constructive-io/fn-generator test |
| 92 | + pnpm --filter @constructive-io/fn-client test |
| 93 | + for pkg in fn-types fn-app fn-runtime fn-generator fn-client fn-cli; do |
| 94 | + (cd "packages/$pkg" && pnpm publish --dry-run --no-git-checks --access public) |
| 95 | + done |
| 96 | + ``` |
| 97 | +3. Tag and push: |
| 98 | + ```bash |
| 99 | + git tag fn-v0.1.0 |
| 100 | + git push origin fn-v0.1.0 |
| 101 | + ``` |
| 102 | +4. CI publishes in dependency order: `fn-types` → `fn-app` → `fn-runtime` → `fn-generator` → `fn-client` → `fn-cli`. |
| 103 | + |
| 104 | +You can also run the workflow with `workflow_dispatch` (default `dry_run: true`) to verify packing before tagging. |
| 105 | + |
| 106 | +## Verification checklist (manual, before first release) |
| 107 | + |
| 108 | +- [ ] **Snapshot regression**: `pnpm --filter @constructive-io/fn-generator test` passes (asserts byte-identical output vs `scripts/generate.ts`). |
| 109 | +- [ ] **Job-registry tests**: `pnpm exec jest tests/integration/job-registry.test.ts` — six cases pass. |
| 110 | +- [ ] **Brasilia E2E**: with the live k8s stack running (`make skaffold-dev`), `pnpm test:e2e` still picks up jobs end-to-end. |
| 111 | +- [ ] **Scratch repo**: in a fresh `/tmp/test-fn-app` repo, `pnpm add -D @constructive-io/fn-cli && pnpm add @constructive-io/fn-runtime`, add `functions/hello/handler.{json,ts}`, run `fn generate && fn build && fn manifest`. Confirm output is sensible and `docker build -f generated/hello/Dockerfile .` succeeds. |
| 112 | +- [ ] **Hub integration**: in `constructive-hub/istanbul`, `pnpm bootstrap && pnpm start` still launches `send-email-link` and processes a job (the hub does not yet consume the new toolkit; this confirms Wave 1-3 didn't regress the existing submodule path). |
| 113 | + |
| 114 | +## Deferred follow-ups (not in this branch) |
| 115 | + |
| 116 | +- **Wave 4c — replace hand-written `k8s/base/functions/*.yaml` with generator output**. The hand-written manifests carry mailgun secrets, dry-run env vars, and a different image strategy (single bundled image, args-driven entry vs per-function image with Dockerfile CMD). Migrating safely requires either teaching `KnativeServiceBuilder` to emit those fields or providing a Kustomize patch overlay. Tracked separately. |
| 117 | +- **fn.config.ts/.js loading** — JSON only for now. Adding `.ts` requires an `esbuild`/`jiti` loader. |
| 118 | +- **`fn init` and `fn dockerfile` / `fn k8s` standalone subcommands** — the underlying builders exist (`buildPackages`, `buildSkaffold`); these are thin CLI wrappers to add later. |
| 119 | +- **Templates packaging** — currently `templatesDir` is a constructor option pointing at the host repo's `templates/`. A future change can ship templates inside `fn-generator` (or a separate `fn-templates` package) so customer repos don't need their own copy. |
0 commit comments