create-nwire 0.12.0 → 0.13.0
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 +9 -2
- package/package.json +1 -1
- package/templates/enterprise/AGENTS.md +43 -0
- package/templates/enterprise/__tests__/auto-moderate.test.ts +22 -6
- package/templates/enterprise/__tests__/submit-flow.test.ts +22 -6
- package/templates/enterprise/_npmrc +4 -0
- package/templates/enterprise/_pnpm-workspace.yaml +6 -0
- package/templates/enterprise/app/api.ts +3 -1
- package/templates/enterprise/app/app.ts +23 -15
- package/templates/enterprise/app/main.ts +20 -47
- 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 +27 -25
- 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 +13 -10
- 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 +6 -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 +37 -0
- package/templates/minimal/__tests__/hello.test.ts +3 -3
- package/templates/minimal/_npmrc +4 -0
- package/templates/minimal/_pnpm-workspace.yaml +6 -0
- package/templates/minimal/app/app.ts +21 -0
- package/templates/minimal/app/main.ts +12 -26
- package/templates/minimal/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/templates/minimal/package.json +10 -7
- package/templates/service/AGENTS.md +64 -0
- package/templates/service/__tests__/todo-api.test.ts +5 -5
- package/templates/service/_npmrc +4 -0
- package/templates/service/_pnpm-workspace.yaml +6 -0
- package/templates/service/app/app.ts +25 -0
- package/templates/service/app/errors/todo-errors.ts +2 -2
- package/templates/service/app/main.ts +22 -34
- package/templates/service/app/resources/todo.ts +1 -1
- package/templates/service/app/store/todo-store.ts +9 -25
- 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 +11 -8
- package/templates/service/tsconfig.json +1 -1
|
@@ -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,6 @@
|
|
|
1
|
+
# pnpm 11 reads build-allow + run settings here (not the package.json
|
|
2
|
+
# "pnpm" field). esbuild backs vite-node; allow its build, and skip the
|
|
3
|
+
# pre-run deps check so `pnpm dev` runs clean right after install.
|
|
4
|
+
onlyBuiltDependencies:
|
|
5
|
+
- esbuild
|
|
6
|
+
verifyDepsBeforeRun: 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
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* contract, not invented per call site.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { defineError } from "@nwire/
|
|
16
|
+
import { defineError } from "@nwire/handler";
|
|
17
17
|
|
|
18
18
|
/** A user looked up a todo by id and we don't have it. */
|
|
19
19
|
export const TodoNotFound = defineError({
|
|
@@ -45,4 +45,4 @@ export const NoUserId = defineError({
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
// Re-export common framework errors so handlers import from one place.
|
|
48
|
-
export { Unauthorized, Forbidden, NotFound, Gone } from "@nwire/
|
|
48
|
+
export { Unauthorized, Forbidden, NotFound, Gone } from "@nwire/handler";
|
|
@@ -1,44 +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
|
|
9
5
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
6
|
+
* curl -X POST http://localhost:3000/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:3000/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
|
-
|
|
41
|
-
|
|
42
|
-
.mount(app)
|
|
43
|
-
.run();
|
|
44
|
-
}
|
|
29
|
+
await endpoint(cfg.name, { port: cfg.port, banner: cfg.banner })
|
|
30
|
+
.use(httpKoa({ prefix: http.prefix, middleware: [requireUser] }))
|
|
31
|
+
.mount(app)
|
|
32
|
+
.run();
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* the same — `provide("todos", { boot, shutdown })`. Handlers always do
|
|
15
15
|
* `resolve("todos")` and get whatever the plugin booted.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* This template uses the simpler container-register-then-provide path
|
|
18
18
|
* from `main.ts` (no createApp). The plugin form is exported so it's
|
|
19
|
-
* one rename away once you
|
|
19
|
+
* one rename away once you add the forge battery.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { randomUUID } from "node:crypto";
|
|
@@ -96,35 +96,19 @@ export class TodoStore {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
// ─── The plugin (optional — used when you
|
|
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
|
-
* L2 main.ts uses the simpler `container.register(...)` path because we
|
|
113
|
-
* don't have a forge app yet. The plugin is exported so L4 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,23 +4,26 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "
|
|
8
|
-
"test": "vitest run"
|
|
7
|
+
"dev": "nwire dev",
|
|
8
|
+
"test": "vitest run",
|
|
9
|
+
"doctor": "nwire doctor",
|
|
10
|
+
"studio": "nwire studio",
|
|
11
|
+
"cache": "nwire cache"
|
|
9
12
|
},
|
|
10
13
|
"dependencies": {
|
|
11
|
-
"@nwire/app": "^0.
|
|
12
|
-
"@nwire/endpoint": "^0.
|
|
13
|
-
"@nwire/handler": "^0.
|
|
14
|
-
"@nwire/koa": "^0.
|
|
15
|
-
"@nwire/wires": "^0.
|
|
14
|
+
"@nwire/app": "^0.13.0",
|
|
15
|
+
"@nwire/endpoint": "^0.13.0",
|
|
16
|
+
"@nwire/handler": "^0.13.0",
|
|
17
|
+
"@nwire/koa": "^0.13.0",
|
|
18
|
+
"@nwire/wires": "^0.13.0",
|
|
16
19
|
"koa": "^2.16.1",
|
|
17
20
|
"zod": "^4.0.0"
|
|
18
21
|
},
|
|
19
22
|
"devDependencies": {
|
|
23
|
+
"@nwire/cli": "^0.13.0",
|
|
20
24
|
"@types/koa": "^2.15.0",
|
|
21
25
|
"@types/node": "^22.19.9",
|
|
22
26
|
"typescript": "^5.9.0",
|
|
23
|
-
"vite-node": "^3.2.4",
|
|
24
27
|
"vitest": "^4.0.18"
|
|
25
28
|
}
|
|
26
29
|
}
|