create-nwire 0.12.1 → 0.13.1
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/dist/index.d.ts +1 -1
- package/dist/index.js +6 -2
- package/package.json +1 -1
- package/templates/enterprise/AGENTS.md +11 -11
- package/templates/enterprise/README.md +15 -10
- package/templates/enterprise/__tests__/auto-moderate.test.ts +22 -6
- package/templates/enterprise/__tests__/submit-flow.test.ts +22 -6
- package/templates/enterprise/app/api.ts +5 -1
- package/templates/enterprise/app/app.ts +23 -15
- package/templates/enterprise/app/main.ts +20 -45
- package/templates/enterprise/config/app.ts +18 -0
- package/templates/enterprise/config/env.ts +32 -0
- package/templates/enterprise/config/http.ts +15 -0
- package/templates/enterprise/modules/posts/actions/approve-post.ts +4 -3
- package/templates/enterprise/modules/posts/actions/reject-post.ts +3 -3
- package/templates/enterprise/modules/posts/actions/submit-post.ts +4 -3
- package/templates/enterprise/modules/posts/events/post-was-approved.ts +1 -1
- package/templates/enterprise/modules/posts/events/post-was-rejected.ts +2 -1
- package/templates/enterprise/modules/posts/events/post-was-submitted.ts +2 -1
- package/templates/enterprise/modules/posts/projections/queue-dashboard.ts +34 -27
- package/templates/enterprise/modules/posts/queries/posts-by-author.ts +7 -5
- package/templates/enterprise/modules/posts/routes/approve-post.ts +3 -3
- package/templates/enterprise/modules/posts/routes/get-post.ts +3 -3
- package/templates/enterprise/modules/posts/routes/list-queue.ts +4 -4
- package/templates/enterprise/modules/posts/routes/posts-by-author.ts +28 -0
- package/templates/enterprise/modules/posts/routes/reject-post.ts +3 -3
- package/templates/enterprise/modules/posts/routes/submit-post.ts +3 -3
- package/templates/enterprise/modules/posts/workflows/auto-moderate.ts +3 -3
- package/templates/enterprise/package.json +10 -11
- package/templates/enterprise/tsconfig.json +1 -1
- package/templates/mcp/AGENTS.md +73 -0
- package/templates/mcp/__tests__/mcp-server.test.ts +205 -0
- package/templates/mcp/_gitignore +5 -0
- package/templates/mcp/_npmrc +4 -0
- package/templates/mcp/_pnpm-workspace.yaml +12 -0
- package/templates/mcp/app/app.ts +24 -0
- package/templates/mcp/app/main.ts +197 -0
- package/templates/mcp/app/store/facts-store.ts +17 -0
- package/templates/mcp/app/tools/add-fact.ts +27 -0
- package/templates/mcp/app/tools/list-facts.ts +17 -0
- package/templates/mcp/app/tools/lookup-fact.ts +28 -0
- package/templates/mcp/app/tools.ts +19 -0
- package/templates/mcp/config/app.ts +16 -0
- package/templates/mcp/config/env.ts +27 -0
- package/templates/mcp/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/templates/mcp/package.json +25 -0
- package/templates/mcp/tsconfig.json +15 -0
- package/templates/mcp/vitest.config.ts +8 -0
- package/templates/minimal/AGENTS.md +16 -9
- package/templates/minimal/README.md +20 -10
- package/templates/minimal/__tests__/hello.test.ts +3 -3
- package/templates/minimal/app/app.ts +21 -0
- package/templates/minimal/app/main.ts +15 -24
- package/templates/minimal/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/templates/minimal/package.json +7 -8
- package/templates/service/AGENTS.md +13 -11
- package/templates/service/README.md +5 -3
- package/templates/service/__tests__/todo-api.test.ts +5 -5
- package/templates/service/app/app.ts +25 -0
- package/templates/service/app/main.ts +20 -30
- package/templates/service/app/store/todo-store.ts +6 -22
- package/templates/service/config/app.ts +18 -0
- package/templates/service/config/env.ts +32 -0
- package/templates/service/config/http.ts +15 -0
- package/templates/service/package.json +8 -9
- package/templates/service/tsconfig.json +1 -1
|
@@ -14,7 +14,9 @@ import { post } from "@nwire/wires/http";
|
|
|
14
14
|
import { httpKoa } from "@nwire/koa";
|
|
15
15
|
|
|
16
16
|
const app = createApp({ appName: "svc" });
|
|
17
|
-
app.wire(post("/things", { body: z.object({ name: z.string() }) }), async (input) => ({
|
|
17
|
+
app.wire(post("/things", { body: z.object({ name: z.string() }) }), async (input) => ({
|
|
18
|
+
ok: input.name,
|
|
19
|
+
}));
|
|
18
20
|
await endpoint("svc", { port: 3000 }).use(httpKoa()).mount(app).run();
|
|
19
21
|
```
|
|
20
22
|
|
|
@@ -26,15 +28,15 @@ their request shapes in `app/resources/`, errors in `app/errors/`.
|
|
|
26
28
|
The single most common mistake is importing a primitive from the wrong
|
|
27
29
|
package. Use exactly:
|
|
28
30
|
|
|
29
|
-
| Primitive
|
|
30
|
-
|
|
31
|
-
| `createApp`, `appCompose`, `definePlugin`
|
|
32
|
-
| `endpoint`
|
|
33
|
-
| `get` `post` `put` `patch` `del`
|
|
34
|
-
| `httpKoa`
|
|
35
|
-
| `defineHandler`, `defineResource`, `defineError`, `Unauthorized`/`Forbidden`/`NotFound`/`Conflict`/`BadRequest` | `@nwire/handler`
|
|
36
|
-
| `defineEvent`
|
|
37
|
-
| `defineAction`, `defineActor`, `defineProjection`, `defineQuery`, `defineWorkflow`, `
|
|
31
|
+
| Primitive | Package |
|
|
32
|
+
| --------------------------------------------------------------------------------------------------------------- | ------------------- |
|
|
33
|
+
| `createApp`, `appCompose`, `definePlugin` | `@nwire/app` |
|
|
34
|
+
| `endpoint` | `@nwire/endpoint` |
|
|
35
|
+
| `get` `post` `put` `patch` `del` | `@nwire/wires/http` |
|
|
36
|
+
| `httpKoa` | `@nwire/koa` |
|
|
37
|
+
| `defineHandler`, `defineResource`, `defineError`, `Unauthorized`/`Forbidden`/`NotFound`/`Conflict`/`BadRequest` | `@nwire/handler` |
|
|
38
|
+
| `defineEvent` | `@nwire/messages` |
|
|
39
|
+
| `defineAction`, `defineActor`, `defineProjection`, `defineQuery`, `defineWorkflow`, `forgePlugins` | `@nwire/forge` |
|
|
38
40
|
|
|
39
41
|
`defineResource` and `defineError` are in **`@nwire/handler`**, not
|
|
40
42
|
`@nwire/forge`. This service template does not depend on `@nwire/forge` —
|
|
@@ -56,7 +58,7 @@ listener file (not `on` — `on` is for lifecycle hooks).
|
|
|
56
58
|
|
|
57
59
|
## Commands
|
|
58
60
|
|
|
59
|
-
- `pnpm dev` —
|
|
61
|
+
- `pnpm dev` — wire + Studio on http://localhost:4000.
|
|
60
62
|
- `pnpm test` — run the test suite.
|
|
61
63
|
- `pnpm doctor` — health-check the project setup.
|
|
62
64
|
- `pnpm studio` — open the live trace/inspect console.
|
|
@@ -16,18 +16,20 @@ pnpm dev
|
|
|
16
16
|
|
|
17
17
|
## Try
|
|
18
18
|
|
|
19
|
+
`pnpm dev` serves the wire + Studio on http://localhost:4000.
|
|
20
|
+
|
|
19
21
|
```bash
|
|
20
22
|
# create a todo
|
|
21
|
-
curl -X POST http://localhost:
|
|
23
|
+
curl -X POST http://localhost:4000/api/todos \
|
|
22
24
|
-H "content-type: application/json" \
|
|
23
25
|
-H "x-user-id: alice" \
|
|
24
26
|
-d '{"text":"buy milk"}'
|
|
25
27
|
|
|
26
28
|
# list this user's todos
|
|
27
|
-
curl -H "x-user-id: alice" http://localhost:
|
|
29
|
+
curl -H "x-user-id: alice" http://localhost:4000/api/todos
|
|
28
30
|
|
|
29
31
|
# complete one
|
|
30
|
-
curl -X POST http://localhost:
|
|
32
|
+
curl -X POST http://localhost:4000/api/todos/<id>/complete \
|
|
31
33
|
-H "x-user-id: alice"
|
|
32
34
|
```
|
|
33
35
|
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Smoke test — the four CRUD operations + the requireUser middleware.
|
|
3
3
|
*
|
|
4
|
-
* Boots the app
|
|
5
|
-
*
|
|
4
|
+
* Boots the app from `app.ts` (the pure value) against an ephemeral port.
|
|
5
|
+
* `app` is complete on import — routes are wired in the module body of
|
|
6
|
+
* `app.ts`. Never imports `main.ts` — that file runs a real server and
|
|
7
|
+
* hangs the test runner.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
9
11
|
import { endpoint } from "@nwire/endpoint";
|
|
10
12
|
import { httpKoa } from "@nwire/koa";
|
|
11
|
-
import {
|
|
13
|
+
import { app } from "../app/app";
|
|
12
14
|
import { requireUser } from "../app/middleware/require-user";
|
|
13
15
|
|
|
14
16
|
let running: Awaited<ReturnType<ReturnType<typeof endpoint>["run"]>>;
|
|
15
|
-
let app: ReturnType<typeof buildApp>;
|
|
16
17
|
let url: string;
|
|
17
18
|
|
|
18
19
|
beforeAll(async () => {
|
|
19
|
-
app = buildApp();
|
|
20
20
|
const koa = httpKoa({ port: 0, prefix: "/api", middleware: [requireUser] });
|
|
21
21
|
running = await endpoint("todo-test", {
|
|
22
22
|
exitOnShutdown: false,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App — the bounded context value.
|
|
3
|
+
*
|
|
4
|
+
* `createApp` installs plugins and wires all routes in this module body.
|
|
5
|
+
* The exported `app` is COMPLETE on import — no caller needs to wire or
|
|
6
|
+
* start it. No ports are bound here; that is `main.ts`'s job.
|
|
7
|
+
*
|
|
8
|
+
* Tests import `{ app }` from this file and build an isolated endpoint.
|
|
9
|
+
* Never import `main.ts` from tests: it boots a real server and hangs
|
|
10
|
+
* the runner.
|
|
11
|
+
*
|
|
12
|
+
* Graduate: swap `TodoStore` for `@nwire/drizzle` or `@nwire/mongo`.
|
|
13
|
+
* The plugin shape (`provide("todos", { boot, shutdown, healthCheck })`)
|
|
14
|
+
* stays the same; handlers keep calling `resolve("todos")`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createApp } from "@nwire/app";
|
|
18
|
+
import { todoStorePlugin } from "./store/todo-store";
|
|
19
|
+
import { wires } from "./api";
|
|
20
|
+
|
|
21
|
+
export const app = createApp({ appName: "{{PROJECT_NAME}}" }).with(todoStorePlugin);
|
|
22
|
+
|
|
23
|
+
for (const { binding, handler } of wires) {
|
|
24
|
+
app.wire(binding, handler);
|
|
25
|
+
}
|
|
@@ -1,42 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Entry —
|
|
2
|
+
* Entry — the only file that boots a real HTTP server.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* the TodoStore on the container.
|
|
6
|
-
* 2. app.wire(...) pairs every route binding with its handler.
|
|
7
|
-
* 3. endpoint().use(httpKoa({ middleware: [requireUser] })).mount(app).run()
|
|
8
|
-
* runs the HTTP adapter under graceful drain + K8s probes.
|
|
4
|
+
* pnpm dev # wire + Studio on http://localhost:4000
|
|
9
5
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
6
|
+
* curl -X POST http://localhost:4000/api/todos \
|
|
7
|
+
* -H "content-type: application/json" \
|
|
8
|
+
* -H "x-user-id: alice" \
|
|
9
|
+
* -d '{"text":"buy milk"}'
|
|
10
|
+
* curl -H "x-user-id: alice" http://localhost:4000/api/todos
|
|
11
|
+
*
|
|
12
|
+
* `app` is a pure value — routes are wired in `./app` on import.
|
|
13
|
+
* This file adds the one side effect: binding a port and serving traffic.
|
|
14
|
+
* Tests import `{ app }` from `./app` and build their own ephemeral
|
|
15
|
+
* endpoint — never import this file from tests or it boots a server and
|
|
16
|
+
* hangs the runner.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
|
-
import { createApp, definePlugin } from "@nwire/app";
|
|
19
19
|
import { endpoint } from "@nwire/endpoint";
|
|
20
20
|
import { httpKoa } from "@nwire/koa";
|
|
21
|
-
|
|
22
|
-
import {
|
|
21
|
+
import { appConfig } from "../config/app";
|
|
22
|
+
import { httpConfig } from "../config/http";
|
|
23
23
|
import { requireUser } from "./middleware/require-user";
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
const todoStorePlugin = definePlugin("todo-store", ({ bind }) => {
|
|
27
|
-
bind("todos", new TodoStore());
|
|
28
|
-
});
|
|
24
|
+
import { app } from "./app";
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for (const { binding, handler } of wires) {
|
|
33
|
-
app.wire(binding, handler);
|
|
34
|
-
}
|
|
35
|
-
return app;
|
|
36
|
-
}
|
|
26
|
+
const cfg = appConfig();
|
|
27
|
+
const http = httpConfig();
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.use(httpKoa({ prefix: "/api", middleware: [requireUser] }))
|
|
29
|
+
await endpoint(cfg.name, { port: cfg.port, banner: cfg.banner })
|
|
30
|
+
.use(httpKoa({ prefix: http.prefix, middleware: [requireUser] }))
|
|
41
31
|
.mount(app)
|
|
42
32
|
.run();
|
|
@@ -99,32 +99,16 @@ export class TodoStore {
|
|
|
99
99
|
// ─── The plugin (optional — used when you add the forge battery) ────────
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Plugin shape: `
|
|
102
|
+
* Plugin shape: `bind("todos", store)`.
|
|
103
103
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
104
|
+
* `bind` registers the store on the container under the name we chose.
|
|
105
|
+
* From any handler: `resolve("todos")`. For lifecycle teardown, use
|
|
106
|
+
* `dispose(() => ...)` — called on graceful shutdown.
|
|
107
107
|
*
|
|
108
108
|
* This same shape works whether the store is in-memory (here), a Pool
|
|
109
109
|
* (Postgres via Drizzle), or a MongoClient. The plugin wrapper handles
|
|
110
110
|
* the lifecycle; the store implementation handles its own concerns.
|
|
111
|
-
*
|
|
112
|
-
* this template's main.ts uses the simpler `container.register(...)` path because we
|
|
113
|
-
* don't have a forge app yet. The plugin is exported so forge graduation
|
|
114
|
-
* is `import { todoStorePlugin } from "./store/todo-store"`.
|
|
115
111
|
*/
|
|
116
|
-
export const todoStorePlugin = definePlugin("todos", ({
|
|
117
|
-
|
|
118
|
-
boot: () => new TodoStore(),
|
|
119
|
-
shutdown: (store) => {
|
|
120
|
-
// For an in-memory store there's nothing to close. A real adapter
|
|
121
|
-
// would call `pool.end()` / `client.close()` here.
|
|
122
|
-
void store;
|
|
123
|
-
},
|
|
124
|
-
healthCheck: (store) => {
|
|
125
|
-
// Trivial — proves the store is alive. A real adapter would
|
|
126
|
-
// SELECT 1 against the DB. Lightship calls this on every /ready.
|
|
127
|
-
if (store.size() < 0) throw new Error("store corrupt");
|
|
128
|
-
},
|
|
129
|
-
});
|
|
112
|
+
export const todoStorePlugin = definePlugin("todos", ({ bind }) => {
|
|
113
|
+
bind("todos", new TodoStore());
|
|
130
114
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-level settings — the process identity and lifecycle budgets the
|
|
3
|
+
* endpoint owns. A function of the typed `env`, so the same config file
|
|
4
|
+
* reads differently per environment without an `if` in sight.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { env, type Env } from "./env";
|
|
8
|
+
|
|
9
|
+
export const appConfig = (e: Env = env) => ({
|
|
10
|
+
/** Endpoint + app name — shows in the banner, logs, and telemetry. */
|
|
11
|
+
name: "{{PROJECT_NAME}}",
|
|
12
|
+
/** Port the HTTP transport binds. */
|
|
13
|
+
port: e.PORT,
|
|
14
|
+
/** Print the boot banner outside of tests. */
|
|
15
|
+
banner: e.NODE_ENV !== "test",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type AppConfig = ReturnType<typeof appConfig>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed environment — read `process.env` once, through a schema, and never
|
|
3
|
+
* touch it again from app code. Missing or malformed values fail loudly at
|
|
4
|
+
* boot with a readable message, not three layers deep at first request.
|
|
5
|
+
*
|
|
6
|
+
* `config/*.ts` files take this typed `env` and shape it into the settings
|
|
7
|
+
* each concern needs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
const EnvSchema = z.object({
|
|
13
|
+
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
|
14
|
+
PORT: z.coerce.number().int().min(0).max(65_535).default(3000),
|
|
15
|
+
HTTP_PREFIX: z.string().default("/api"),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type Env = z.output<typeof EnvSchema>;
|
|
19
|
+
|
|
20
|
+
export function loadEnv(source: NodeJS.ProcessEnv = process.env): Env {
|
|
21
|
+
const parsed = EnvSchema.safeParse(source);
|
|
22
|
+
if (!parsed.success) {
|
|
23
|
+
const summary = parsed.error.issues
|
|
24
|
+
.map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`)
|
|
25
|
+
.join("\n");
|
|
26
|
+
throw new Error(`Invalid environment:\n${summary}`);
|
|
27
|
+
}
|
|
28
|
+
return parsed.data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The validated environment, loaded once. */
|
|
32
|
+
export const env: Env = loadEnv();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP transport settings — prefix, OpenAPI, and the dev inspect surface
|
|
3
|
+
* Studio reads. A function of the typed `env`, like every config file here.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { env, type Env } from "./env";
|
|
7
|
+
|
|
8
|
+
export const httpConfig = (e: Env = env) => ({
|
|
9
|
+
/** Mounted under this prefix — e.g. `POST /api/todos`. */
|
|
10
|
+
prefix: e.HTTP_PREFIX,
|
|
11
|
+
/** Expose detailed errors in dev; redact in production. */
|
|
12
|
+
exposeErrors: e.NODE_ENV !== "production",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type HttpConfig = ReturnType<typeof httpConfig>;
|
|
@@ -4,27 +4,26 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "
|
|
7
|
+
"dev": "nwire dev",
|
|
8
8
|
"test": "vitest run",
|
|
9
9
|
"doctor": "nwire doctor",
|
|
10
10
|
"studio": "nwire studio",
|
|
11
11
|
"cache": "nwire cache"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@nwire/app": "^0.
|
|
15
|
-
"@nwire/endpoint": "^0.
|
|
16
|
-
"@nwire/handler": "^0.
|
|
17
|
-
"@nwire/koa": "^0.
|
|
18
|
-
"@nwire/wires": "^0.
|
|
14
|
+
"@nwire/app": "^0.13.1",
|
|
15
|
+
"@nwire/endpoint": "^0.13.1",
|
|
16
|
+
"@nwire/handler": "^0.13.1",
|
|
17
|
+
"@nwire/koa": "^0.13.1",
|
|
18
|
+
"@nwire/wires": "^0.13.1",
|
|
19
19
|
"koa": "^2.16.1",
|
|
20
20
|
"zod": "^4.0.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
+
"@nwire/cli": "^0.13.1",
|
|
23
24
|
"@types/koa": "^2.15.0",
|
|
24
25
|
"@types/node": "^22.19.9",
|
|
25
26
|
"typescript": "^5.9.0",
|
|
26
|
-
"
|
|
27
|
-
"vitest": "^4.0.18",
|
|
28
|
-
"@nwire/cli": "^0.12.1"
|
|
27
|
+
"vitest": "^4.0.18"
|
|
29
28
|
}
|
|
30
29
|
}
|