@webstir-io/webstir-backend 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +427 -0
- package/dist/build/artifacts.d.ts +113 -0
- package/dist/build/artifacts.js +53 -0
- package/dist/build/entries.d.ts +1 -0
- package/dist/build/entries.js +17 -0
- package/dist/build/pipeline.d.ts +31 -0
- package/dist/build/pipeline.js +424 -0
- package/dist/cache/diff.d.ts +4 -0
- package/dist/cache/diff.js +114 -0
- package/dist/cache/reporters.d.ts +12 -0
- package/dist/cache/reporters.js +23 -0
- package/dist/diagnostics/summary.d.ts +6 -0
- package/dist/diagnostics/summary.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/manifest/pipeline.d.ts +13 -0
- package/dist/manifest/pipeline.js +224 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +101 -0
- package/dist/scaffold/assets.d.ts +2 -0
- package/dist/scaffold/assets.js +77 -0
- package/dist/testing/context.d.ts +3 -0
- package/dist/testing/context.js +14 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +208 -0
- package/dist/testing/types.d.ts +28 -0
- package/dist/testing/types.js +1 -0
- package/dist/watch.d.ts +8 -0
- package/dist/watch.js +159 -0
- package/dist/workspace.d.ts +4 -0
- package/dist/workspace.js +15 -0
- package/package.json +74 -0
- package/scripts/publish.sh +99 -0
- package/scripts/smoke.mjs +241 -0
- package/scripts/update-contract.sh +122 -0
- package/src/build/artifacts.ts +67 -0
- package/src/build/entries.ts +19 -0
- package/src/build/pipeline.ts +507 -0
- package/src/cache/diff.ts +128 -0
- package/src/cache/reporters.ts +41 -0
- package/src/diagnostics/summary.ts +32 -0
- package/src/index.ts +2 -0
- package/src/manifest/pipeline.ts +270 -0
- package/src/provider.ts +124 -0
- package/src/scaffold/assets.ts +81 -0
- package/src/testing/context.d.ts +3 -0
- package/src/testing/context.js +14 -0
- package/src/testing/context.ts +17 -0
- package/src/testing/index.d.ts +6 -0
- package/src/testing/index.js +208 -0
- package/src/testing/index.ts +252 -0
- package/src/testing/types.d.ts +28 -0
- package/src/testing/types.js +1 -0
- package/src/testing/types.ts +32 -0
- package/src/watch.ts +177 -0
- package/src/workspace.ts +22 -0
- package/templates/backend/.env.example +13 -0
- package/templates/backend/auth/adapter.ts +160 -0
- package/templates/backend/db/connection.ts +99 -0
- package/templates/backend/db/migrate.ts +231 -0
- package/templates/backend/db/migrations/0001-example.ts +17 -0
- package/templates/backend/db/types.d.ts +2 -0
- package/templates/backend/env.ts +174 -0
- package/templates/backend/functions/hello/index.ts +29 -0
- package/templates/backend/index.ts +532 -0
- package/templates/backend/jobs/nightly/index.ts +28 -0
- package/templates/backend/jobs/runtime.ts +103 -0
- package/templates/backend/jobs/scheduler.ts +193 -0
- package/templates/backend/module.ts +87 -0
- package/templates/backend/observability/logger.ts +24 -0
- package/templates/backend/observability/metrics.ts +78 -0
- package/templates/backend/server/fastify.ts +288 -0
- package/templates/backend/tsconfig.json +19 -0
- package/tests/cacheReporter.test.js +89 -0
- package/tests/envLoader.test.js +64 -0
- package/tests/integration.test.js +108 -0
- package/tests/manifest.test.js +159 -0
- package/tests/watch.test.js +100 -0
- package/tsconfig.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# @webstir-io/webstir-backend
|
|
2
|
+
|
|
3
|
+
Backend build orchestration for Webstir workspaces. The package exposes a `ModuleProvider` that type‑checks with TypeScript, builds with esbuild, collects build artifacts, and returns diagnostics for the Webstir CLI and installers.
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
- Experimental provider for the Webstir ecosystem — APIs, defaults, and behavior may change between releases while things stabilize.
|
|
8
|
+
- Not yet recommended for production workloads; treat it as a learning and exploration tool.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
1. **Authenticate to GitHub Packages**
|
|
13
|
+
Configure user-level auth (recommended) or set an env var:
|
|
14
|
+
- User config (`~/.npmrc`):
|
|
15
|
+
```ini
|
|
16
|
+
@webstir-io:registry=https://npm.pkg.github.com
|
|
17
|
+
//npm.pkg.github.com/:_authToken=${GH_PACKAGES_TOKEN}
|
|
18
|
+
```
|
|
19
|
+
- Or export a token (CI uses `NODE_AUTH_TOKEN`):
|
|
20
|
+
```bash
|
|
21
|
+
export NODE_AUTH_TOKEN="$GH_PACKAGES_TOKEN"
|
|
22
|
+
```
|
|
23
|
+
Consumers need `read:packages`; publishers also require `write:packages`.
|
|
24
|
+
2. **Install**
|
|
25
|
+
```bash
|
|
26
|
+
npm install @webstir-io/webstir-backend
|
|
27
|
+
```
|
|
28
|
+
3. **Run a build**
|
|
29
|
+
```ts
|
|
30
|
+
import { backendProvider } from '@webstir-io/webstir-backend';
|
|
31
|
+
|
|
32
|
+
const { manifest } = await backendProvider.build({
|
|
33
|
+
workspaceRoot: '/absolute/path/to/workspace',
|
|
34
|
+
env: { WEBSTIR_MODULE_MODE: 'build' },
|
|
35
|
+
incremental: true
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
console.log(manifest.entryPoints);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Node.js **20.18.x** or newer.
|
|
42
|
+
|
|
43
|
+
## Community & Support
|
|
44
|
+
|
|
45
|
+
- Code of Conduct: https://github.com/webstir-io/.github/blob/main/CODE_OF_CONDUCT.md
|
|
46
|
+
- Contributing guidelines: https://github.com/webstir-io/.github/blob/main/CONTRIBUTING.md
|
|
47
|
+
- Security policy and disclosure process: https://github.com/webstir-io/.github/blob/main/SECURITY.md
|
|
48
|
+
- Support expectations and contact channels: https://github.com/webstir-io/.github/blob/main/SUPPORT.md
|
|
49
|
+
|
|
50
|
+
## Workspace Layout
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
workspace/
|
|
54
|
+
src/backend/
|
|
55
|
+
tsconfig.json
|
|
56
|
+
index.ts # optional monolithic server entry
|
|
57
|
+
functions/*/index.ts # optional function handlers
|
|
58
|
+
jobs/*/index.ts # optional job/worker entries
|
|
59
|
+
handlers/
|
|
60
|
+
tests/
|
|
61
|
+
build/backend/... # compiled JS output
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The provider expects a standard workspace layout and performs two steps:
|
|
65
|
+
|
|
66
|
+
1) Type checking via `tsc -p src/backend/tsconfig.json --noEmit` (can be skipped in dev; see below)
|
|
67
|
+
2) Build via esbuild into `build/backend`:
|
|
68
|
+
- build/test: transpile only (no bundle), sourcemaps on
|
|
69
|
+
- publish: bundle each entry, externalize node_modules, minify, strip comments
|
|
70
|
+
|
|
71
|
+
## Provider Contract
|
|
72
|
+
|
|
73
|
+
`backendProvider` implements `ModuleProvider` from `@webstir-io/module-contract`:
|
|
74
|
+
|
|
75
|
+
- `metadata` — package id, version, kind (`backend`), CLI compatibility, Node range.
|
|
76
|
+
- `resolveWorkspace({ workspaceRoot })` — returns canonical source/build/test roots.
|
|
77
|
+
- `build(options)` — type‑checks with `tsc --noEmit`, then runs esbuild. In `build`/`test` mode it transpiles without bundling; in `publish` it bundles workspace code, externalizes `node_modules`, minifies, strips comments, and defines `NODE_ENV=production`. Artifacts are gathered and a manifest describing entry points, diagnostics, and the module contract manifest is returned.
|
|
78
|
+
- `getScaffoldAssets()` — returns starter files to bootstrap a backend workspace:
|
|
79
|
+
- `src/backend/tsconfig.json` (NodeNext, outDir `build/backend`)
|
|
80
|
+
- `src/backend/index.ts` (built-in HTTP server with `/api/health`, `/healthz`, `/readyz`, manifest summaries, `x-request-id` propagation, and automatic module route mounting)
|
|
81
|
+
- `src/backend/module.ts` (optional manifest + handler example the server loads automatically)
|
|
82
|
+
- `src/backend/server/fastify.ts` (optional Fastify server scaffold)
|
|
83
|
+
|
|
84
|
+
### Fastify Scaffold (optional)
|
|
85
|
+
|
|
86
|
+
The default HTTP server handles `/api/health`, readiness logging, and auto-mounts the compiled `module.ts` handlers. If you prefer Fastify’s plugin ecosystem or need advanced routing features, you can swap the entry for the Fastify scaffold:
|
|
87
|
+
|
|
88
|
+
- Install Fastify in your workspace:
|
|
89
|
+
```bash
|
|
90
|
+
npm i fastify
|
|
91
|
+
```
|
|
92
|
+
- Import and start it from your `src/backend/index.ts`:
|
|
93
|
+
```ts
|
|
94
|
+
// src/backend/index.ts
|
|
95
|
+
import { start } from './server/fastify';
|
|
96
|
+
start().catch((err) => { console.error(err); process.exit(1); });
|
|
97
|
+
```
|
|
98
|
+
- Or run it directly after a build:
|
|
99
|
+
```bash
|
|
100
|
+
node build/backend/server/fastify.js
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Note: The package’s smoke test temporarily installs Fastify only to type‑check the optional scaffold. Normal users do not need Fastify unless they choose to use this server. In CI or offline environments, set `WEBSTIR_BACKEND_SMOKE_FASTIFY=skip` to bypass the Fastify install and type‑check.
|
|
104
|
+
|
|
105
|
+
When present, the Fastify scaffold will also attempt to auto‑mount any compiled module routes it finds under `build/backend/module(.js)`. Export your module definition as `module`, `moduleDefinition`, or the `default` export from `src/backend/module.ts` and build; the server will attach handlers using the route metadata.
|
|
106
|
+
|
|
107
|
+
### Server runtime baseline
|
|
108
|
+
|
|
109
|
+
The default `src/backend/index.ts` entry (and the optional Fastify scaffold) share the same runtime guarantees:
|
|
110
|
+
|
|
111
|
+
- Route auto-mounting: any `module.ts` routes are compiled, logged, and attached on startup with manifest summaries (name, version, route count, capabilities).
|
|
112
|
+
- Health probes: `/api/health` (for the orchestrator), `/healthz` (generic health), and `/readyz` (status + manifest summary). The CLI still waits for `API server running` before proxying requests.
|
|
113
|
+
- Structured logging: every request gets a `pino` child logger that carries `requestId`, method, path, and route metadata. Logs emit as JSON so downstream tooling can parse them easily.
|
|
114
|
+
- Request context: handlers receive `params`, `query`, `body`, `env`, `logger`, `request`, `reply`, `requestId`, and `now()` helpers that align with the `RequestContext` shape from `@webstir-io/module-contract`.
|
|
115
|
+
- Request IDs: each response sets `x-request-id` and the context/logger include the same identifier so you can correlate logs.
|
|
116
|
+
- Failure safety: handler exceptions are caught and surfaced as `{ error: 'internal_error' }` without tearing down the process.
|
|
117
|
+
|
|
118
|
+
Stick with the built-in server while exploring the manifest helpers, then drop in the Fastify scaffold when you need its plugin ecosystem—the readiness + manifest wiring stays the same.
|
|
119
|
+
|
|
120
|
+
### Secrets & auth adapters
|
|
121
|
+
|
|
122
|
+
The backend template now ships a lightweight auth adapter so you can secure routes without wiring a full identity provider on day one:
|
|
123
|
+
|
|
124
|
+
- **Environment-driven secrets** — populate `.env.local`/`.env` with `AUTH_JWT_SECRET` (required for bearer tokens), optional `AUTH_JWT_ISSUER` / `AUTH_JWT_AUDIENCE`, and comma/space-delimited `AUTH_SERVICE_TOKENS`. An example lives in `templates/backend/.env.example`.
|
|
125
|
+
- **Bearer verification (HS256)** — when `AUTH_JWT_SECRET` is set, incoming `Authorization: Bearer <token>` headers are validated using HMAC-SHA256. Matching issuer/audience claims are enforced if you provide them. On success, `ctx.auth` includes `userId`, `email`, `scopes`, `roles`, and the raw claims payload.
|
|
126
|
+
- **Service tokens** — internal callers can present `X-Service-Token` or `X-API-Key` values that match `AUTH_SERVICE_TOKENS`. Successful matches yield a `ctx.auth` context with the `service` scope so you can distinguish automated jobs from end users.
|
|
127
|
+
- **Route ergonomics** — the module template now demonstrates gating access on `ctx.auth` and sets the `auth` capability in the manifest so downstream tooling knows the module expects identity context.
|
|
128
|
+
- Install `pino` in your workspace (`npm install pino`) before running the scaffold; the template server imports it directly.
|
|
129
|
+
|
|
130
|
+
This adapter is intentionally simple (HS256 only) but gives you a hook to plug in third-party IdPs: generate/sign tokens there, supply the shared secret via env, and the scaffold will populate `ctx.auth` for every route.
|
|
131
|
+
|
|
132
|
+
### Observability & metrics
|
|
133
|
+
|
|
134
|
+
- **Structured logs** — set `LOG_LEVEL` (default `info`) and optionally `LOG_SERVICE_NAME`. Every request emits a `request.completed` entry with status code and latency, plus rich metadata (`requestId`, method, route).
|
|
135
|
+
- **Metrics** — enable with `METRICS_ENABLED=on` (default) and tune the rolling window via `METRICS_WINDOW` (number of recent durations to keep). The server tracks totals, error counts, average latency, and p95 latency.
|
|
136
|
+
- **Endpoints** — `/metrics` returns the snapshot JSON; `/readyz` now includes the same metrics summary alongside manifest info so orchestrators and dashboards can consume a single payload.
|
|
137
|
+
|
|
138
|
+
Install `pino` (and optionally `pino-pretty` for local formatting) in any workspace that uses the backend template; no other setup is required.
|
|
139
|
+
|
|
140
|
+
### Jobs & scheduling
|
|
141
|
+
|
|
142
|
+
- Define jobs via `webstir add-job <name> [--schedule "<cron>"] [--description "..."] [--priority <number|label>]`. The CLI creates `src/backend/jobs/<name>/index.ts` and records metadata in `webstir.moduleManifest.jobs` in `package.json`.
|
|
143
|
+
- The template provides a zero-config job loader (`src/backend/jobs/runtime.ts`) and a lightweight scheduler/runner (`build/backend/jobs/scheduler.js`). Use it to explore your jobs without wiring a full queue:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm install pino # already needed for the server
|
|
147
|
+
npx tsx src/backend/jobs/scheduler.ts --list
|
|
148
|
+
node build/backend/jobs/scheduler.js --job nightly
|
|
149
|
+
node build/backend/jobs/scheduler.js --watch # runs @hourly/@daily/@weekly/@reboot or rate(...) jobs
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- `/readyz` surfaces manifest job counts, and `node build/backend/jobs/<name>/index.js` remains the quickest way to execute a single job in isolation.
|
|
153
|
+
- Cron expressions recorded in the manifest are left untouched so you can plug them into your real scheduler (Temporal, Quartz, Cloud Scheduler, etc.). The built-in watcher supports the `@hourly`, `@daily`, `@weekly`, `@reboot`, and `rate(n units)` patterns for basic local loops; fall back to external tooling for full cron semantics.
|
|
154
|
+
|
|
155
|
+
### Database & migrations
|
|
156
|
+
|
|
157
|
+
- `DATABASE_URL` defaults to `file:./data/dev.sqlite`. Point it at Postgres (`postgres://...`) or another SQLite file as needed. Override the tracking table via `DATABASE_MIGRATIONS_TABLE` (defaults to `_webstir_migrations`).
|
|
158
|
+
- `src/backend/db/connection.ts` exposes a tiny helper that connects to SQLite (via `better-sqlite3`) or Postgres (`pg`). Install whichever driver you need in your workspace: `npm install better-sqlite3` for the default flow or `npm install pg` for Postgres.
|
|
159
|
+
- Drop SQL/TypeScript migrations under `src/backend/db/migrations/*.ts`, exporting `id`, `up`, and optional `down`.
|
|
160
|
+
- Run migrations with:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npx tsx src/backend/db/migrate.ts --list
|
|
164
|
+
npx tsx src/backend/db/migrate.ts # apply pending migrations
|
|
165
|
+
npx tsx src/backend/db/migrate.ts --down --steps 1
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- The runner logs each migration, records history in `DATABASE_MIGRATIONS_TABLE`, and works the same way once compiled (`node build/backend/db/migrate.js ...`).
|
|
169
|
+
|
|
170
|
+
### Module Manifest Integration
|
|
171
|
+
|
|
172
|
+
When `build()` completes, it now returns a `ModuleBuildManifest` with a `module` property that matches the contract introduced in `@webstir-io/module-contract@0.1.5`. The provider looks for module metadata in the workspace’s `package.json` under `webstir.moduleManifest`. If present, the object is validated against the shared `moduleManifestSchema`; otherwise, sane defaults are generated from the workspace package name/version.
|
|
173
|
+
|
|
174
|
+
```jsonc
|
|
175
|
+
// workspace/package.json
|
|
176
|
+
{
|
|
177
|
+
"name": "@demo/accounts",
|
|
178
|
+
"version": "0.1.0",
|
|
179
|
+
"webstir": {
|
|
180
|
+
"moduleManifest": {
|
|
181
|
+
"contractVersion": "1.0.0",
|
|
182
|
+
"name": "@demo/accounts",
|
|
183
|
+
"version": "0.1.0",
|
|
184
|
+
"capabilities": ["auth", "views"],
|
|
185
|
+
"routes": [],
|
|
186
|
+
"views": []
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
If the manifest fails validation, the provider emits a diagnostic and falls back to a minimal contract (name/version/kind only). This keeps consuming tooling resilient while still surfacing issues to the developer.
|
|
193
|
+
|
|
194
|
+
After a build, the provider also tries to load `build/backend/module.js` (compiled from `src/backend/module.ts`). Export a `createModule(...)` definition as `module`, `moduleDefinition`, or `default` to have routes, views, and capabilities hydrated automatically.
|
|
195
|
+
|
|
196
|
+
#### ts-rest Router Example
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// src/backend/module.ts
|
|
200
|
+
import { initContract } from '@ts-rest/core';
|
|
201
|
+
import { createModule, fromTsRestRouter, CONTRACT_VERSION, type RequestContext } from '@webstir-io/module-contract';
|
|
202
|
+
import { z } from 'zod';
|
|
203
|
+
|
|
204
|
+
const c = initContract();
|
|
205
|
+
|
|
206
|
+
const accountSchema = z.object({
|
|
207
|
+
id: z.string().uuid(),
|
|
208
|
+
email: z.string().email()
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const router = c.router({
|
|
212
|
+
list: c.query({
|
|
213
|
+
path: '/accounts',
|
|
214
|
+
method: 'GET',
|
|
215
|
+
responses: {
|
|
216
|
+
200: z.object({ data: z.array(accountSchema) })
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
219
|
+
detail: c.query({
|
|
220
|
+
path: '/accounts/:id',
|
|
221
|
+
method: 'GET',
|
|
222
|
+
pathParams: z.object({ id: z.string().uuid() }),
|
|
223
|
+
responses: {
|
|
224
|
+
200: accountSchema,
|
|
225
|
+
404: z.null()
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const routeSpecs = fromTsRestRouter<RequestContext>({
|
|
231
|
+
router,
|
|
232
|
+
baseName: 'accounts',
|
|
233
|
+
createRoute: ({ keyPath }) => ({
|
|
234
|
+
handler: async (ctx) => {
|
|
235
|
+
if (keyPath.at(-1) === 'detail') {
|
|
236
|
+
const row = await ctx.db.accounts.findById(ctx.params.id);
|
|
237
|
+
return row
|
|
238
|
+
? { status: 200, body: row }
|
|
239
|
+
: { status: 404, errors: [{ code: 'not_found', message: 'Account not found' }] };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const rows = await ctx.db.accounts.list();
|
|
243
|
+
return { status: 200, body: { data: rows } };
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
export const module = createModule({
|
|
249
|
+
manifest: {
|
|
250
|
+
contractVersion: CONTRACT_VERSION,
|
|
251
|
+
name: '@demo/accounts',
|
|
252
|
+
version: '0.1.0',
|
|
253
|
+
kind: 'backend',
|
|
254
|
+
capabilities: ['db', 'auth'],
|
|
255
|
+
routes: routeSpecs.map((route) => route.definition)
|
|
256
|
+
},
|
|
257
|
+
routes: routeSpecs
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
When `npm run build` completes, the provider detects `build/backend/module.js`, hydrates the manifest with the `routes` metadata above, and returns it to the orchestrator alongside the compiled entry points.
|
|
262
|
+
|
|
263
|
+
#### Module Definition Only Example
|
|
264
|
+
|
|
265
|
+
If you prefer to skip `createModule()` during early development, you can export a simple object from `module.ts` and the provider will still merge its manifest metadata:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// src/backend/module.ts
|
|
269
|
+
export const module = {
|
|
270
|
+
manifest: {
|
|
271
|
+
contractVersion: '1.0.0',
|
|
272
|
+
name: '@demo/simple-module',
|
|
273
|
+
version: '0.1.0',
|
|
274
|
+
kind: 'backend',
|
|
275
|
+
capabilities: ['search'],
|
|
276
|
+
routes: [{ method: 'GET', path: '/simple' }]
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Backend Testing Harness
|
|
282
|
+
|
|
283
|
+
Backend route tests can now launch the compiled server directly through the `@webstir-io/webstir-backend/testing` entry point. Import the helper inside your compiled backend tests (for example under `src/backend/tests/**`) and wrap each suite with `backendTest()`:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
import { assert } from '@webstir-io/webstir-testing';
|
|
287
|
+
import { backendTest } from '@webstir-io/webstir-backend/testing';
|
|
288
|
+
|
|
289
|
+
backendTest('health endpoint responds', async (ctx) => {
|
|
290
|
+
const response = await ctx.request('/api/health');
|
|
291
|
+
const body = await response.json();
|
|
292
|
+
assert.equal(body.ok, true, 'Expected health endpoint to return { ok: true }');
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The harness:
|
|
297
|
+
|
|
298
|
+
- Spins up `build/backend/index.js` (or a custom entry via `WEBSTIR_BACKEND_TEST_ENTRY`) with the same env wiring used during builds.
|
|
299
|
+
- Waits for the readiness log (`API server running` by default) before running your assertions.
|
|
300
|
+
- Exposes the hydrated `ModuleManifest` via `ctx.manifest` and provides a `request()` helper that targets the running server.
|
|
301
|
+
- Shuts the server down once the backend runtime finishes so `webstir test` / `webstir watch` can continue without orphaned processes.
|
|
302
|
+
|
|
303
|
+
Environment variables such as `WEBSTIR_BACKEND_TEST_PORT`, `WEBSTIR_BACKEND_TEST_READY`, and `WEBSTIR_BACKEND_TEST_MANIFEST` let you customize the port, readiness text, and manifest path when the defaults do not fit.
|
|
304
|
+
|
|
305
|
+
This keeps your manifest co-located with runtime code while the provider handles validation and hydration.
|
|
306
|
+
|
|
307
|
+
### Environment Management
|
|
308
|
+
|
|
309
|
+
- `src/backend/env.ts` loads `.env.local` (if present) followed by `.env`, merges values into `process.env`, and exposes a typed `loadEnv()` helper.
|
|
310
|
+
- A `.env.example` file is scaffolded at the workspace root—copy it to `.env`/`.env.local`, fill in secrets (e.g., `API_BASE_URL`, `DATABASE_URL`, `JWT_SECRET`), and adjust `loadEnv()` to require the variables your backend needs.
|
|
311
|
+
- The default HTTP server (and Fastify scaffold) calls `loadEnv()` before binding, so the same config is available inside route handlers. Use `ctx.env.require('JWT_SECRET')` to fetch validated values.
|
|
312
|
+
|
|
313
|
+
### Multiple Entry Points
|
|
314
|
+
The provider discovers these entries automatically (all optional):
|
|
315
|
+
|
|
316
|
+
- `src/backend/index.{ts,tsx,js,mjs}`
|
|
317
|
+
- `src/backend/functions/*/index.{ts,tsx,js,mjs}`
|
|
318
|
+
- `src/backend/jobs/*/index.{ts,tsx,js,mjs}`
|
|
319
|
+
|
|
320
|
+
Outputs mirror the source layout under `build/backend/**/index.js`. The manifest lists relative `index.js` paths for all entries.
|
|
321
|
+
|
|
322
|
+
Artifacts are returned as absolute paths so installers can copy or upload them. A missing `index.js` triggers a warning diagnostic.
|
|
323
|
+
|
|
324
|
+
## Internal Helper Layout
|
|
325
|
+
|
|
326
|
+
- `src/workspace.ts` — resolves source/build/test roots and normalizes `WEBSTIR_MODULE_MODE`.
|
|
327
|
+
- `src/build/pipeline.ts` — runs type-check, esbuild (incremental + publish), and compiles optional `module.ts`.
|
|
328
|
+
- `src/build/artifacts.ts` — collects build outputs (bundles/assets) and derives the manifest entry list.
|
|
329
|
+
- `src/manifest/pipeline.ts` — hydrates the module manifest from `package.json` + `build/backend/module.js`, validating with the shared contract.
|
|
330
|
+
- `src/cache/diff.ts` — records `.webstir` cache files for outputs/manifest digests and emits diff diagnostics.
|
|
331
|
+
- `src/diagnostics/summary.ts` — common diagnostic helpers (log-level filtering, entry bucket summaries).
|
|
332
|
+
- `src/scaffold/assets.ts` — backend scaffold definitions consumed by the provider and tests.
|
|
333
|
+
|
|
334
|
+
## NPM Scripts
|
|
335
|
+
|
|
336
|
+
| Script | Description |
|
|
337
|
+
|--------|-------------|
|
|
338
|
+
| `npm run build` | Compiles provider TypeScript from `src/` into `dist/`. |
|
|
339
|
+
| `npm test` | Builds and runs Node's test runner over `tests/**/*.test.js`. |
|
|
340
|
+
| `npm run smoke` | Quick end-to-end check: scaffolds a temp workspace and runs build/publish via the provider. |
|
|
341
|
+
| `npm run clean` | Removes `dist/`. |
|
|
342
|
+
|
|
343
|
+
The published package ships prebuilt JavaScript and type definitions in `dist/`.
|
|
344
|
+
|
|
345
|
+
## Maintainer Workflow
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
npm install
|
|
349
|
+
npm run clean # remove dist artifacts
|
|
350
|
+
npm run build # emits dist/
|
|
351
|
+
npm run test # runs unit/integration tests
|
|
352
|
+
npm run smoke
|
|
353
|
+
# Release helper (bumps version, pushes tags to trigger release workflow)
|
|
354
|
+
npm run release -- patch
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
- Add tests under `tests/**/*.test.ts` and wire them into `npm test` once the backend runtime is ready.
|
|
358
|
+
- Ensure CI runs `npm ci`, `npm run clean`, `npm run build`, `npm run test`, and `npm run smoke` before publish.
|
|
359
|
+
- Publishing targets GitHub Packages via `publishConfig.registry`.
|
|
360
|
+
- Use `npm run release -- <patch|minor|major|x.y.z>` to bump the version, build, test, run the smoke check, and push tags to trigger the release workflow.
|
|
361
|
+
|
|
362
|
+
## Troubleshooting
|
|
363
|
+
|
|
364
|
+
- “TypeScript config not found at src/backend/tsconfig.json; skipping type-check.” — esbuild can still build, but path aliases and stricter checks may be skipped. Add a workspace `src/backend/tsconfig.json`.
|
|
365
|
+
- “No backend entry point found” — ensure `src/backend/index.ts` (or `index.js`) exists. The provider looks for `index.*` and emits `build/backend/index.js`.
|
|
366
|
+
- esbuild warnings/errors are surfaced as diagnostics with file locations when available.
|
|
367
|
+
|
|
368
|
+
CI notes
|
|
369
|
+
- Package CI runs clean + build + tests + smoke on PRs and main.
|
|
370
|
+
|
|
371
|
+
Dev tips
|
|
372
|
+
- Fast iteration: set `WEBSTIR_BACKEND_TYPECHECK=skip` to bypass type-checking during `build`/`test` mode. Type-checks always run for `publish`.
|
|
373
|
+
- Publish sourcemaps: set `WEBSTIR_BACKEND_SOURCEMAPS=on` (before `webstir publish` or provider builds) to bundle `.js.map` files alongside the minified output. The maps are excluded by default to keep bundle sizes lean.
|
|
374
|
+
- **`TypeScript config not found` warning** — ensure `src/backend/tsconfig.json` exists.
|
|
375
|
+
- **`Backend TypeScript compilation failed`** — inspect diagnostics (stderr/stdout captured in the manifest) and rerun `tsc -p`.
|
|
376
|
+
- **No backend entry point found** — confirm `build/backend/index.js` exists after compilation or adjust the build output.
|
|
377
|
+
|
|
378
|
+
## License
|
|
379
|
+
|
|
380
|
+
MIT © Webstir
|
|
381
|
+
### Watch Mode (developer convenience)
|
|
382
|
+
|
|
383
|
+
Start incremental builds with type-checking in the background:
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
npm run dev # type-check + transpile on change
|
|
387
|
+
npm run dev:fast # faster DX: skip tsc in watch
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Notes
|
|
391
|
+
- Set `WEBSTIR_BACKEND_DIAG_MAX=<n>` to cap how many esbuild diagnostics print per rebuild (default: 20 in standalone watch, 50 in provider builds invoked by the orchestrator).
|
|
392
|
+
- Publish still enforces `tsc --noEmit` even if you skip type-checking in watch.
|
|
393
|
+
- After each rebuild you’ll see concise summaries and a manifest glance, for example:
|
|
394
|
+
- `watch:esbuild 0 error(s), N warning(s) in X ms`
|
|
395
|
+
- `watch:manifest routes=N views=M [capabilities]`
|
|
396
|
+
- Cache parity: once esbuild finishes, watch mode writes the same `.webstir/backend-outputs.json` / `backend-manifest-digest.json` files and logs diff summaries (changed bundles, added/removed routes/views) just like non-watch builds. This keeps downstream tooling in sync during long-running dev sessions.
|
|
397
|
+
- Set `WEBSTIR_BACKEND_CACHE_LOG=off` (or `false/0/skip`) to update the `.webstir` cache quietly without emitting diff diagnostics—handy for very chatty watch sessions.
|
|
398
|
+
|
|
399
|
+
Or programmatically:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
import { startBackendWatch } from '@webstir-io/webstir-backend';
|
|
403
|
+
|
|
404
|
+
const handle = await startBackendWatch({
|
|
405
|
+
workspaceRoot: '/abs/path/to/workspace',
|
|
406
|
+
env: { WEBSTIR_MODULE_MODE: 'build' }
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// later
|
|
410
|
+
await handle.stop();
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Functions & Jobs (scaffolding)
|
|
414
|
+
|
|
415
|
+
The provider ships example entries you can copy into a fresh workspace:
|
|
416
|
+
|
|
417
|
+
- `src/backend/functions/hello/index.ts` — a simple function entry
|
|
418
|
+
- `src/backend/jobs/nightly/index.ts` — a simple job entry
|
|
419
|
+
|
|
420
|
+
If you use `getScaffoldAssets()` programmatically, these templates are included alongside `tsconfig.json` and `index.ts`.
|
|
421
|
+
|
|
422
|
+
### Dev runner readiness
|
|
423
|
+
|
|
424
|
+
- The backend template listens on `process.env.PORT` (default `4000`) and logs `API server running` when ready.
|
|
425
|
+
- The orchestrator's dev server waits for that readiness line and proxies `/api/*` to your Node server.
|
|
426
|
+
- Health probes: `/api/health` (orchestrator compatibility) mirrors `/healthz`, while `/readyz` exposes the readiness state plus the current manifest summary for external monitors.
|
|
427
|
+
- If you switch to a framework (e.g., Fastify), keep the same behavior: listen on `process.env.PORT`, expose the same endpoints, and print `API server running` once the server is listening.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { ModuleArtifact, ModuleDiagnostic, ModuleManifest } from '@webstir-io/module-contract';
|
|
2
|
+
export declare function collectArtifacts(buildRoot: string, includeSourceMaps: boolean): Promise<ModuleArtifact[]>;
|
|
3
|
+
export declare function createBuildManifest(buildRoot: string, artifacts: readonly ModuleArtifact[], diagnostics: ModuleDiagnostic[], moduleManifest: ModuleManifest): {
|
|
4
|
+
entryPoints: string[];
|
|
5
|
+
staticAssets: never[];
|
|
6
|
+
diagnostics: ModuleDiagnostic[];
|
|
7
|
+
module: {
|
|
8
|
+
kind: "frontend" | "backend";
|
|
9
|
+
name: string;
|
|
10
|
+
contractVersion: string;
|
|
11
|
+
version: string;
|
|
12
|
+
capabilities?: string[] | undefined;
|
|
13
|
+
assets?: string[] | undefined;
|
|
14
|
+
middlewares?: string[] | undefined;
|
|
15
|
+
routes?: {
|
|
16
|
+
name: string;
|
|
17
|
+
path: string;
|
|
18
|
+
method: "GET" | "POST" | "DELETE" | "PUT" | "PATCH" | "HEAD" | "OPTIONS";
|
|
19
|
+
summary?: string | undefined;
|
|
20
|
+
description?: string | undefined;
|
|
21
|
+
tags?: string[] | undefined;
|
|
22
|
+
errors?: {
|
|
23
|
+
code: "validation" | "auth" | "not_found" | "domain" | "conflict" | "internal";
|
|
24
|
+
message: string;
|
|
25
|
+
details?: unknown;
|
|
26
|
+
cause?: unknown;
|
|
27
|
+
correlationId?: string | undefined;
|
|
28
|
+
}[] | undefined;
|
|
29
|
+
input?: {
|
|
30
|
+
params?: {
|
|
31
|
+
kind: "zod" | "json-schema" | "ts-rest";
|
|
32
|
+
name: string;
|
|
33
|
+
source?: string | undefined;
|
|
34
|
+
} | undefined;
|
|
35
|
+
query?: {
|
|
36
|
+
kind: "zod" | "json-schema" | "ts-rest";
|
|
37
|
+
name: string;
|
|
38
|
+
source?: string | undefined;
|
|
39
|
+
} | undefined;
|
|
40
|
+
body?: {
|
|
41
|
+
kind: "zod" | "json-schema" | "ts-rest";
|
|
42
|
+
name: string;
|
|
43
|
+
source?: string | undefined;
|
|
44
|
+
} | undefined;
|
|
45
|
+
headers?: {
|
|
46
|
+
kind: "zod" | "json-schema" | "ts-rest";
|
|
47
|
+
name: string;
|
|
48
|
+
source?: string | undefined;
|
|
49
|
+
} | undefined;
|
|
50
|
+
} | undefined;
|
|
51
|
+
output?: {
|
|
52
|
+
body: {
|
|
53
|
+
kind: "zod" | "json-schema" | "ts-rest";
|
|
54
|
+
name: string;
|
|
55
|
+
source?: string | undefined;
|
|
56
|
+
};
|
|
57
|
+
status?: number | undefined;
|
|
58
|
+
headers?: {
|
|
59
|
+
kind: "zod" | "json-schema" | "ts-rest";
|
|
60
|
+
name: string;
|
|
61
|
+
source?: string | undefined;
|
|
62
|
+
} | undefined;
|
|
63
|
+
} | undefined;
|
|
64
|
+
ssg?: {
|
|
65
|
+
revalidateSeconds?: number | undefined;
|
|
66
|
+
} | undefined;
|
|
67
|
+
renderMode?: "ssg" | "ssr" | "spa" | undefined;
|
|
68
|
+
staticPaths?: string[] | undefined;
|
|
69
|
+
}[] | undefined;
|
|
70
|
+
views?: {
|
|
71
|
+
name: string;
|
|
72
|
+
path: string;
|
|
73
|
+
params?: {
|
|
74
|
+
kind: "zod" | "json-schema" | "ts-rest";
|
|
75
|
+
name: string;
|
|
76
|
+
source?: string | undefined;
|
|
77
|
+
} | undefined;
|
|
78
|
+
summary?: string | undefined;
|
|
79
|
+
description?: string | undefined;
|
|
80
|
+
tags?: string[] | undefined;
|
|
81
|
+
ssg?: {
|
|
82
|
+
revalidateSeconds?: number | undefined;
|
|
83
|
+
} | undefined;
|
|
84
|
+
renderMode?: "ssg" | "ssr" | "spa" | undefined;
|
|
85
|
+
staticPaths?: string[] | undefined;
|
|
86
|
+
data?: {
|
|
87
|
+
kind: "zod" | "json-schema" | "ts-rest";
|
|
88
|
+
name: string;
|
|
89
|
+
source?: string | undefined;
|
|
90
|
+
} | undefined;
|
|
91
|
+
}[] | undefined;
|
|
92
|
+
jobs?: {
|
|
93
|
+
name: string;
|
|
94
|
+
schedule?: string | undefined;
|
|
95
|
+
priority?: string | number | undefined;
|
|
96
|
+
}[] | undefined;
|
|
97
|
+
events?: {
|
|
98
|
+
name: string;
|
|
99
|
+
description?: string | undefined;
|
|
100
|
+
payload?: {
|
|
101
|
+
kind: "zod" | "json-schema" | "ts-rest";
|
|
102
|
+
name: string;
|
|
103
|
+
source?: string | undefined;
|
|
104
|
+
} | undefined;
|
|
105
|
+
}[] | undefined;
|
|
106
|
+
services?: {
|
|
107
|
+
name: string;
|
|
108
|
+
description?: string | undefined;
|
|
109
|
+
}[] | undefined;
|
|
110
|
+
init?: string | undefined;
|
|
111
|
+
dispose?: string | undefined;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import { pushEntryBucketSummary } from '../diagnostics/summary.js';
|
|
5
|
+
export async function collectArtifacts(buildRoot, includeSourceMaps) {
|
|
6
|
+
const patterns = ['**/*.js'];
|
|
7
|
+
if (includeSourceMaps) {
|
|
8
|
+
patterns.push('**/*.js.map');
|
|
9
|
+
}
|
|
10
|
+
const matches = new Set();
|
|
11
|
+
for (const pattern of patterns) {
|
|
12
|
+
const files = await glob(pattern, {
|
|
13
|
+
cwd: buildRoot,
|
|
14
|
+
nodir: true,
|
|
15
|
+
dot: false
|
|
16
|
+
});
|
|
17
|
+
for (const relativePath of files) {
|
|
18
|
+
matches.add(relativePath);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return Array.from(matches).map((relativePath) => ({
|
|
22
|
+
path: path.join(buildRoot, relativePath),
|
|
23
|
+
type: relativePath.endsWith('.map') ? 'asset' : 'bundle'
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
export function createBuildManifest(buildRoot, artifacts, diagnostics, moduleManifest) {
|
|
27
|
+
const entryPoints = [];
|
|
28
|
+
for (const artifact of artifacts) {
|
|
29
|
+
const relative = path.relative(buildRoot, artifact.path);
|
|
30
|
+
if (relative.endsWith('index.js')) {
|
|
31
|
+
entryPoints.push(relative);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (entryPoints.length === 0) {
|
|
35
|
+
const defaultEntry = path.join(buildRoot, 'index.js');
|
|
36
|
+
if (existsSync(defaultEntry)) {
|
|
37
|
+
entryPoints.push(path.relative(buildRoot, defaultEntry));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
diagnostics.push({
|
|
41
|
+
severity: 'warn',
|
|
42
|
+
message: 'No backend entry point found (expected index.js).'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
pushEntryBucketSummary(diagnostics, entryPoints);
|
|
47
|
+
return {
|
|
48
|
+
entryPoints,
|
|
49
|
+
staticAssets: [],
|
|
50
|
+
diagnostics,
|
|
51
|
+
module: moduleManifest
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function discoverEntryPoints(sourceRoot: string): Promise<string[]>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { glob } from 'glob';
|
|
3
|
+
export async function discoverEntryPoints(sourceRoot) {
|
|
4
|
+
const patterns = [
|
|
5
|
+
'index.{ts,tsx,js,mjs}',
|
|
6
|
+
'functions/*/index.{ts,tsx,js,mjs}',
|
|
7
|
+
'jobs/*/index.{ts,tsx,js,mjs}'
|
|
8
|
+
];
|
|
9
|
+
const entries = new Set();
|
|
10
|
+
for (const pattern of patterns) {
|
|
11
|
+
const matches = await glob(pattern, { cwd: sourceRoot, nodir: true, dot: false });
|
|
12
|
+
for (const rel of matches) {
|
|
13
|
+
entries.add(path.join(sourceRoot, rel));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return Array.from(entries);
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ModuleDiagnostic } from '@webstir-io/module-contract';
|
|
2
|
+
import type { BackendBuildMode } from '../workspace.js';
|
|
3
|
+
export interface BackendBuildPipelineOptions {
|
|
4
|
+
readonly sourceRoot: string;
|
|
5
|
+
readonly buildRoot: string;
|
|
6
|
+
readonly tsconfigPath: string;
|
|
7
|
+
readonly mode: BackendBuildMode;
|
|
8
|
+
readonly env: Record<string, string | undefined>;
|
|
9
|
+
readonly incremental: boolean;
|
|
10
|
+
readonly diagnostics: ModuleDiagnostic[];
|
|
11
|
+
}
|
|
12
|
+
export interface BackendBuildPipelineResult {
|
|
13
|
+
readonly entryPoints: readonly string[];
|
|
14
|
+
readonly outputs?: Record<string, number>;
|
|
15
|
+
readonly includePublishSourcemaps: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function runBackendBuildPipeline(options: BackendBuildPipelineOptions): Promise<BackendBuildPipelineResult>;
|
|
18
|
+
export declare function shouldTypeCheck(mode: BackendBuildMode, env: Record<string, string | undefined>): boolean;
|
|
19
|
+
interface SupportFileBuildOptions {
|
|
20
|
+
readonly sourceFile: string;
|
|
21
|
+
readonly sourceRoot: string;
|
|
22
|
+
readonly buildRoot: string;
|
|
23
|
+
readonly tsconfigPath: string;
|
|
24
|
+
readonly mode: BackendBuildMode;
|
|
25
|
+
readonly env: Record<string, string | undefined>;
|
|
26
|
+
readonly diagnostics: ModuleDiagnostic[];
|
|
27
|
+
}
|
|
28
|
+
export declare function buildSupportFile(options: SupportFileBuildOptions): Promise<void>;
|
|
29
|
+
export declare function collectOutputSizes(metafile: unknown, buildRoot: string): Record<string, number>;
|
|
30
|
+
export declare function formatEsbuildMessage(msg: any): string;
|
|
31
|
+
export {};
|