@hedwigjs/create-registry 0.1.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/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/index.js +217 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.js +116 -0
- package/dist/prompts.js.map +1 -0
- package/package.json +48 -0
- package/templates/README.md.tmpl +85 -0
- package/templates/_gitignore +3 -0
- package/templates/package.json.tmpl +26 -0
- package/templates/scripts/build.mjs +284 -0
- package/templates/src/domains/.gitkeep +0 -0
- package/templates/src/index.generated.ts +7 -0
- package/templates/src/index.ts +3 -0
- package/templates/src/lib/contract.ts +36 -0
- package/templates/tsconfig.json +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hedwig contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# @hedwigjs/create-registry
|
|
2
|
+
|
|
3
|
+
Optional starter kit. Scaffolds an opinionated topics-registry
|
|
4
|
+
workspace for `@hedwigjs/broker` — a codegen-driven TypeScript package
|
|
5
|
+
where each event lives in its own file and the runtime types
|
|
6
|
+
(`Topic`, `TopicPayloads`, `TOPICS`, `registry`) are generated for you.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# planned CLI, not yet published to npm:
|
|
10
|
+
npm create @hedwigjs/registry my-topics
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> `@hedwigjs/broker` accepts topics from **any source** — Zod,
|
|
14
|
+
> Protobuf, GraphQL, a hand-written `TopicMap`, or a mix. This package
|
|
15
|
+
> is a convenience layer for teams starting fresh in TypeScript. If
|
|
16
|
+
> you already have a contracts pipeline, keep it — see
|
|
17
|
+
> [Bring your own contracts](../../docs/content/guides/bring-your-own-contracts.md).
|
|
18
|
+
|
|
19
|
+
> Pre-release (`0.1.0`, private). Not yet published to npm.
|
|
20
|
+
|
|
21
|
+
**Full project docs & reference stand →** [`../..#readme`](../..#readme)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Table of contents
|
|
26
|
+
|
|
27
|
+
- [What it does](#what-it-does)
|
|
28
|
+
- [Usage](#usage)
|
|
29
|
+
- [What gets generated](#what-gets-generated)
|
|
30
|
+
- [The `EventContract` shape](#the-eventcontract-shape)
|
|
31
|
+
- [Generated exports](#generated-exports)
|
|
32
|
+
- [Using the registry in your app](#using-the-registry-in-your-app)
|
|
33
|
+
- [Adding an event](#adding-an-event)
|
|
34
|
+
- [Versioning workflow](#versioning-workflow)
|
|
35
|
+
- [When to use — and when not to](#when-to-use--and-when-not-to)
|
|
36
|
+
- [License](#license)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## What it does
|
|
41
|
+
|
|
42
|
+
Running the initializer creates a standalone TypeScript package with:
|
|
43
|
+
|
|
44
|
+
- A file convention: one event per file at `src/domains/<domain>/<action>.v<N>.ts`.
|
|
45
|
+
- A tiny `EventContract<Name, Payload>` type.
|
|
46
|
+
- A codegen (`scripts/build.mjs`) that scans `src/domains/`, validates
|
|
47
|
+
names, and writes `src/index.generated.ts` — a composed registry
|
|
48
|
+
plus the exact `Topic` / `TopicPayloads` types `@hedwigjs/broker`
|
|
49
|
+
expects, and a `TOPICS` constant map you can use to avoid string
|
|
50
|
+
typos at call sites.
|
|
51
|
+
|
|
52
|
+
The generated package has **no runtime dependency on `@hedwigjs/*`** —
|
|
53
|
+
it's a plain TS package that ships types and (optional) fixture
|
|
54
|
+
payloads. Consumers can be broker clients, custom pub/sub, tests, or
|
|
55
|
+
docs generators.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm create @hedwigjs/registry <directory> [options]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Interactive by default. Pre-supply flags to skip prompts.
|
|
66
|
+
|
|
67
|
+
| Flag | Purpose |
|
|
68
|
+
| --------------------- | -------------------------------------------------------------------- |
|
|
69
|
+
| `<directory>` | Positional. Target directory for the new package. |
|
|
70
|
+
| `--name <name>` | npm package name (e.g. `@my-org/topics`). Asked interactively if omitted. |
|
|
71
|
+
| `--install` | Run `npm install` in the created directory after scaffold. |
|
|
72
|
+
| `--no-install` | Skip install. |
|
|
73
|
+
| `--yes`, `-y` | Accept all defaults. Requires `<directory>` positional. |
|
|
74
|
+
| `--force`, `-f` | Overwrite a non-empty target directory. |
|
|
75
|
+
| `--help`, `-h` | Show help. |
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# interactive
|
|
79
|
+
npm create @hedwigjs/registry my-topics
|
|
80
|
+
|
|
81
|
+
# fully non-interactive
|
|
82
|
+
npm create @hedwigjs/registry my-topics --name @my-org/topics --yes
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The initializer auto-detects the workspace's package manager
|
|
86
|
+
(`pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, otherwise npm) and
|
|
87
|
+
delegates the install to it.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## What gets generated
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
my-topics/
|
|
95
|
+
├── package.json
|
|
96
|
+
├── tsconfig.json
|
|
97
|
+
├── .gitignore
|
|
98
|
+
├── scripts/
|
|
99
|
+
│ └── build.mjs # codegen: scans src/domains → writes src/index.generated.ts
|
|
100
|
+
└── src/
|
|
101
|
+
├── index.ts # re-exports index.generated (don't edit)
|
|
102
|
+
├── index.generated.ts # AUTO-GENERATED — never hand-edit
|
|
103
|
+
├── domains/ # your event contracts live here
|
|
104
|
+
└── lib/
|
|
105
|
+
└── contract.ts # EventContract<Name, Payload> type (don't edit)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Scripts in the generated package:
|
|
109
|
+
|
|
110
|
+
| Script | What it does |
|
|
111
|
+
| ----------------- | ------------------------------------------------------- |
|
|
112
|
+
| `npm run build` | Codegen + `tsc`. Emits `dist/` ready to publish. |
|
|
113
|
+
| `npm run dev` | Watch codegen + `tsc --watch` in parallel. |
|
|
114
|
+
| `prepublishOnly` | Runs `npm run build` before `npm publish`. |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## The `EventContract` shape
|
|
119
|
+
|
|
120
|
+
Each event file exports a `default` object that `satisfies`
|
|
121
|
+
`EventContract`. Codegen imports every event by default export, so
|
|
122
|
+
**named exports won't be picked up** — always use `export default`.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// src/domains/notification/show.v1.ts
|
|
126
|
+
import type { EventContract } from "../../lib/contract";
|
|
127
|
+
|
|
128
|
+
export default {
|
|
129
|
+
name: "notification.show.v1",
|
|
130
|
+
description: "Show a toast notification.",
|
|
131
|
+
payload: {} as {
|
|
132
|
+
kind: "success" | "info" | "warn" | "error";
|
|
133
|
+
title: string;
|
|
134
|
+
body?: string;
|
|
135
|
+
},
|
|
136
|
+
examples: {
|
|
137
|
+
happy: { kind: "success", title: "Order accepted" },
|
|
138
|
+
error: { kind: "error", title: "Payment failed" },
|
|
139
|
+
},
|
|
140
|
+
} satisfies EventContract;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Fields:
|
|
144
|
+
|
|
145
|
+
| Field | Required | Purpose |
|
|
146
|
+
| ---------------- | -------- | ------------------------------------------------------------------------------------------- |
|
|
147
|
+
| `name` | yes | Topic string. Must match the path: `<domain>/<action>.v<N>.ts` → `"<domain>.<action>.v<N>"`. |
|
|
148
|
+
| `description` | yes | Human-readable. Shown in DevTools and hover cards. |
|
|
149
|
+
| `payload` | yes | Payload type. Idiomatic: `{} as { ... }`. |
|
|
150
|
+
| `examples` | yes | Named fixtures. `examples.happy` is the default seed used by DevTools' Debug tab. |
|
|
151
|
+
| `deprecatedBy` | no | Successor topic name. DevTools surfaces a warning. |
|
|
152
|
+
| `observability` | no | Mark telemetry-only topics so `NACK NO_SUBSCRIBERS` renders neutrally instead of red. |
|
|
153
|
+
|
|
154
|
+
The path-to-name convention is enforced by codegen — a mismatch fails
|
|
155
|
+
the build with an explicit error.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Generated exports
|
|
160
|
+
|
|
161
|
+
Codegen writes `src/index.generated.ts` and `src/index.ts` re-exports
|
|
162
|
+
it. Consumers get four exports from the package root:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { registry, TOPICS, type Topic, type TopicPayloads } from "@my-org/topics";
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Export | Kind | Purpose |
|
|
169
|
+
| ---------------- | ------- | ------------------------------------------------------------------------------------------------ |
|
|
170
|
+
| `Topic` | type | String union of every topic. Drop into `createClient<Topic, TopicPayloads>('id')`. |
|
|
171
|
+
| `TopicPayloads` | type | `{ [topic]: payload }` map. Drop into `initBroker<Topic, TopicPayloads>({...})`. |
|
|
172
|
+
| `TOPICS` | value | SCREAMING_SNAKE_CASE constants like `TOPICS.CART_ITEM_ADDED_V1 === "cart.item-added.v1"`. |
|
|
173
|
+
| `registry` | value | Full `Record<name, EventContract>`. Pass to `<MessageBrokerDevTools registry={registry} />`. |
|
|
174
|
+
|
|
175
|
+
Each event is also importable directly by path — useful when you only
|
|
176
|
+
need one contract:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import CartItemAdded from "@my-org/topics/domains/cart/item-added.v1";
|
|
180
|
+
|
|
181
|
+
cartClient.emit(CartItemAdded.name, { sku: "CROISSANT", qty: 2 });
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Using the registry in your app
|
|
187
|
+
|
|
188
|
+
Boot the broker with the generated types:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { initBroker, createClient } from "@hedwigjs/broker";
|
|
192
|
+
import type { Topic, TopicPayloads } from "@my-org/topics";
|
|
193
|
+
|
|
194
|
+
initBroker<Topic, TopicPayloads>({ history: { enabled: true, maxSize: 200 } });
|
|
195
|
+
|
|
196
|
+
const cartClient = createClient<Topic, TopicPayloads>("cart");
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Rename a topic in one place, and every `emit` / `on` / `request` in
|
|
200
|
+
your codebase lights up in TypeScript.
|
|
201
|
+
|
|
202
|
+
Wire the registry into DevTools for autocomplete and payload prefill:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
import { getBroker } from "@hedwigjs/broker";
|
|
206
|
+
import { MessageBrokerDevTools } from "@hedwigjs/devtools";
|
|
207
|
+
import { registry } from "@my-org/topics";
|
|
208
|
+
|
|
209
|
+
<MessageBrokerDevTools broker={getBroker()} registry={registry} />
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`EventContract` is structurally compatible with the
|
|
213
|
+
`TopicContractInfo` shape DevTools consumes — no adapter needed.
|
|
214
|
+
|
|
215
|
+
Use `TOPICS` at call sites when you'd rather have autocomplete than
|
|
216
|
+
string literals:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
cartClient.emit(TOPICS.CART_ITEM_ADDED_V1, { sku: "CROISSANT", qty: 2 });
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Adding an event
|
|
225
|
+
|
|
226
|
+
1. Create `src/domains/<domain>/<action>.v1.ts` — one event per file.
|
|
227
|
+
2. Fill in `name` (must match the path), `description`, `payload`,
|
|
228
|
+
and at least an `examples.happy` fixture.
|
|
229
|
+
3. `npm run dev` picks it up automatically; `npm run build` produces
|
|
230
|
+
the final `dist/`.
|
|
231
|
+
|
|
232
|
+
Path convention (enforced by codegen):
|
|
233
|
+
|
|
234
|
+
- `<domain>` and `<action>` are kebab-case: `[a-z][a-z0-9-]*`.
|
|
235
|
+
- Nesting depth is exactly one: `src/domains/<domain>/<file>.ts`.
|
|
236
|
+
- File name matches `<action>.v<N>.ts`.
|
|
237
|
+
|
|
238
|
+
Violations fail the build with an explicit message.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Versioning workflow
|
|
243
|
+
|
|
244
|
+
Topics are versioned in the name (`.v1`, `.v2`, …). When a contract
|
|
245
|
+
must change in a breaking way:
|
|
246
|
+
|
|
247
|
+
1. Copy `src/domains/<domain>/<action>.v1.ts` → `<action>.v2.ts`.
|
|
248
|
+
2. Update `name` to `<domain>.<action>.v2` and revise the payload.
|
|
249
|
+
3. In `<action>.v1.ts` add `deprecatedBy: "<domain>.<action>.v2"`.
|
|
250
|
+
4. `npm run build`.
|
|
251
|
+
|
|
252
|
+
Both versions ship side by side in the generated registry. Consumers
|
|
253
|
+
migrate at their own pace; DevTools shows the deprecation warning on
|
|
254
|
+
every v1 message so nothing rots silently.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## When to use — and when not to
|
|
259
|
+
|
|
260
|
+
**Use `@hedwigjs/create-registry` when:**
|
|
261
|
+
|
|
262
|
+
- Greenfield TS project, no existing contract pipeline.
|
|
263
|
+
- You want DevTools autocomplete + payload examples out of the box.
|
|
264
|
+
- You value the enforced path/name convention for grep-ability.
|
|
265
|
+
|
|
266
|
+
**Use your own pipeline when:**
|
|
267
|
+
|
|
268
|
+
- Contracts are already generated (Zod, Protobuf, GraphQL codegen,
|
|
269
|
+
OpenAPI, hand-written `TopicMap`).
|
|
270
|
+
- Non-TS producers publish to the same broker — the source of truth
|
|
271
|
+
lives outside TypeScript.
|
|
272
|
+
- You want a different file layout or naming convention.
|
|
273
|
+
|
|
274
|
+
In both cases, `@hedwigjs/broker` accepts your `Topic` +
|
|
275
|
+
`TopicPayloads` types as generic parameters. Nothing forces the
|
|
276
|
+
starter — see
|
|
277
|
+
[Bring your own contracts](../../docs/content/guides/bring-your-own-contracts.md).
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
MIT.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @hedwigjs/create-registry — initializer entry point.
|
|
4
|
+
*
|
|
5
|
+
* Invoked via `npm create @hedwigjs/registry [<directory>] [options]`.
|
|
6
|
+
*
|
|
7
|
+
* Pipeline:
|
|
8
|
+
* 1. parseArgs(argv) — extract <directory> and flags
|
|
9
|
+
* 2. runPrompts() — interactive UX (or accept pre-supplied)
|
|
10
|
+
* 3. validate target — check existence/empty/--force
|
|
11
|
+
* 4. copyTemplates() — walk templates/, substitute placeholders
|
|
12
|
+
* 5. (optional) npm install
|
|
13
|
+
* 6. print next-step instructions
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
16
|
+
import { dirname, join, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import { runPrompts, confirm } from "./prompts.js";
|
|
20
|
+
// ────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Locate templates/ relative to the compiled bin
|
|
22
|
+
// ────────────────────────────────────────────────────────────────────
|
|
23
|
+
//
|
|
24
|
+
// This file (compiled) lives at <pkg>/dist/index.js
|
|
25
|
+
// templates/ lives at <pkg>/templates/
|
|
26
|
+
// → relative path is ../templates
|
|
27
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const TEMPLATES_DIR = resolve(__dirname, "..", "templates");
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const opts = { yes: false, force: false };
|
|
31
|
+
let i = 0;
|
|
32
|
+
// First positional → directory
|
|
33
|
+
if (argv[0] && !argv[0].startsWith("-")) {
|
|
34
|
+
opts.directory = argv[0];
|
|
35
|
+
i = 1;
|
|
36
|
+
}
|
|
37
|
+
for (; i < argv.length; i++) {
|
|
38
|
+
const arg = argv[i];
|
|
39
|
+
switch (arg) {
|
|
40
|
+
case "--name":
|
|
41
|
+
opts.packageName = argv[++i];
|
|
42
|
+
if (!opts.packageName) {
|
|
43
|
+
throw new Error("--name requires a value");
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
case "--install":
|
|
47
|
+
opts.install = true;
|
|
48
|
+
break;
|
|
49
|
+
case "--no-install":
|
|
50
|
+
opts.install = false;
|
|
51
|
+
break;
|
|
52
|
+
case "--yes":
|
|
53
|
+
case "-y":
|
|
54
|
+
opts.yes = true;
|
|
55
|
+
break;
|
|
56
|
+
case "--force":
|
|
57
|
+
case "-f":
|
|
58
|
+
opts.force = true;
|
|
59
|
+
break;
|
|
60
|
+
case "--help":
|
|
61
|
+
case "-h":
|
|
62
|
+
printHelp();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
// eslint-disable-next-line no-fallthrough
|
|
65
|
+
default:
|
|
66
|
+
if (arg.startsWith("-")) {
|
|
67
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Unexpected positional argument: ${arg}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return opts;
|
|
73
|
+
}
|
|
74
|
+
function printHelp() {
|
|
75
|
+
process.stdout.write(`
|
|
76
|
+
Usage: npm create @hedwigjs/registry [<directory>] [options]
|
|
77
|
+
|
|
78
|
+
Initializes a new directory as a topics registry package for @hedwigjs/broker.
|
|
79
|
+
Created package is fully self-contained — no runtime dependency on @hedwigjs/*.
|
|
80
|
+
|
|
81
|
+
Options:
|
|
82
|
+
--name <name> Package name (e.g. @your_org/topics). Asked interactively if omitted.
|
|
83
|
+
--install Install dependencies after scaffold.
|
|
84
|
+
--no-install Skip npm install.
|
|
85
|
+
--yes, -y Accept all defaults; combine with positional/--name.
|
|
86
|
+
--force, -f Overwrite non-empty target directory.
|
|
87
|
+
--help, -h Show this help.
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
npm create @hedwigjs/registry hse-topics
|
|
91
|
+
npm create @hedwigjs/registry hse-topics --name @your-org/topics --yes
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
// ────────────────────────────────────────────────────────────────────
|
|
95
|
+
// Target directory
|
|
96
|
+
// ────────────────────────────────────────────────────────────────────
|
|
97
|
+
function isDirectoryEmpty(path) {
|
|
98
|
+
try {
|
|
99
|
+
return readdirSync(path).length === 0;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function clearDirectory(path) {
|
|
106
|
+
for (const entry of readdirSync(path)) {
|
|
107
|
+
rmSync(join(path, entry), { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function substitute(content, ctx) {
|
|
111
|
+
const map = ctx;
|
|
112
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
113
|
+
if (key in map)
|
|
114
|
+
return map[key];
|
|
115
|
+
throw new Error(`Unknown placeholder: {{${key}}} in template`);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function copyTemplates(srcDir, destDir, ctx) {
|
|
119
|
+
if (!existsSync(destDir))
|
|
120
|
+
mkdirSync(destDir, { recursive: true });
|
|
121
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
122
|
+
const srcPath = join(srcDir, entry.name);
|
|
123
|
+
let destName = entry.name;
|
|
124
|
+
// Special renames (npm strips .gitignore from published packages)
|
|
125
|
+
if (destName === "_gitignore")
|
|
126
|
+
destName = ".gitignore";
|
|
127
|
+
if (entry.isDirectory()) {
|
|
128
|
+
copyTemplates(srcPath, join(destDir, destName), ctx);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let content = readFileSync(srcPath, "utf-8");
|
|
132
|
+
if (destName.endsWith(".tmpl")) {
|
|
133
|
+
destName = destName.slice(0, -".tmpl".length);
|
|
134
|
+
content = substitute(content, ctx);
|
|
135
|
+
}
|
|
136
|
+
writeFileSync(join(destDir, destName), content);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ────────────────────────────────────────────────────────────────────
|
|
140
|
+
// Package manager detection + install
|
|
141
|
+
// ────────────────────────────────────────────────────────────────────
|
|
142
|
+
function detectPackageManager(startDir) {
|
|
143
|
+
let dir = startDir;
|
|
144
|
+
while (true) {
|
|
145
|
+
if (existsSync(join(dir, "pnpm-lock.yaml")))
|
|
146
|
+
return "pnpm";
|
|
147
|
+
if (existsSync(join(dir, "yarn.lock")))
|
|
148
|
+
return "yarn";
|
|
149
|
+
if (existsSync(join(dir, "package-lock.json")))
|
|
150
|
+
return "npm";
|
|
151
|
+
const parent = dirname(dir);
|
|
152
|
+
if (parent === dir)
|
|
153
|
+
break; // hit filesystem root
|
|
154
|
+
dir = parent;
|
|
155
|
+
}
|
|
156
|
+
return "npm";
|
|
157
|
+
}
|
|
158
|
+
function runInstall(targetDir) {
|
|
159
|
+
const pm = detectPackageManager(dirname(targetDir));
|
|
160
|
+
process.stdout.write(`\nInstalling dependencies with ${pm}...\n`);
|
|
161
|
+
const result = spawnSync(pm, ["install"], {
|
|
162
|
+
cwd: targetDir,
|
|
163
|
+
stdio: "inherit",
|
|
164
|
+
shell: process.platform === "win32",
|
|
165
|
+
});
|
|
166
|
+
if (result.status !== 0) {
|
|
167
|
+
throw new Error(`${pm} install failed (exit code ${result.status})`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ────────────────────────────────────────────────────────────────────
|
|
171
|
+
// Main
|
|
172
|
+
// ────────────────────────────────────────────────────────────────────
|
|
173
|
+
async function main() {
|
|
174
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
175
|
+
const answers = await runPrompts({
|
|
176
|
+
directory: opts.directory,
|
|
177
|
+
packageName: opts.packageName,
|
|
178
|
+
skipPrompts: opts.yes,
|
|
179
|
+
});
|
|
180
|
+
const targetDir = resolve(process.cwd(), answers.directory);
|
|
181
|
+
// Validate target
|
|
182
|
+
if (existsSync(targetDir) && !isDirectoryEmpty(targetDir)) {
|
|
183
|
+
if (!opts.force) {
|
|
184
|
+
throw new Error(`Directory '${answers.directory}' already exists and is not empty.\n` +
|
|
185
|
+
` Run with --force to overwrite.`);
|
|
186
|
+
}
|
|
187
|
+
clearDirectory(targetDir);
|
|
188
|
+
}
|
|
189
|
+
mkdirSync(targetDir, { recursive: true });
|
|
190
|
+
// Copy templates
|
|
191
|
+
copyTemplates(TEMPLATES_DIR, targetDir, {
|
|
192
|
+
PACKAGE_NAME: answers.packageName,
|
|
193
|
+
});
|
|
194
|
+
process.stdout.write(`\n✔ Created ${answers.directory}/\n`);
|
|
195
|
+
// Install dependencies
|
|
196
|
+
let install = opts.install;
|
|
197
|
+
if (install === undefined) {
|
|
198
|
+
install = opts.yes ? true : await confirm("Install dependencies now?", true);
|
|
199
|
+
}
|
|
200
|
+
if (install) {
|
|
201
|
+
runInstall(targetDir);
|
|
202
|
+
}
|
|
203
|
+
// Print next steps
|
|
204
|
+
process.stdout.write(`
|
|
205
|
+
Done! Next steps:
|
|
206
|
+
|
|
207
|
+
cd ${answers.directory}
|
|
208
|
+
${install ? "" : "npm install\n "}npm run dev # watch codegen + tsc
|
|
209
|
+
# Add an event: create src/domains/<domain>/<action>.v1.ts
|
|
210
|
+
# (see the generated README for the file template)
|
|
211
|
+
`);
|
|
212
|
+
}
|
|
213
|
+
main().catch((err) => {
|
|
214
|
+
process.stderr.write(`✗ ${err.message}\n`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
});
|
|
217
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompts for create-registry initializer.
|
|
3
|
+
*
|
|
4
|
+
* Two prompts:
|
|
5
|
+
* 1. Directory name (skipped if passed via argv)
|
|
6
|
+
* 2. Package name (always asked, default derived from directory name)
|
|
7
|
+
*
|
|
8
|
+
* Uses node:readline/promises — no third-party dependency.
|
|
9
|
+
*/
|
|
10
|
+
import { createInterface } from "node:readline/promises";
|
|
11
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
12
|
+
const DIRECTORY_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
13
|
+
const PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
14
|
+
function validateDirectory(value) {
|
|
15
|
+
if (!value)
|
|
16
|
+
return "directory name is required";
|
|
17
|
+
if (value === "." || value === "..")
|
|
18
|
+
return "use an explicit directory name";
|
|
19
|
+
if (value.includes("/") || value.includes("\\"))
|
|
20
|
+
return "must be a single segment (no path separators)";
|
|
21
|
+
if (!DIRECTORY_PATTERN.test(value))
|
|
22
|
+
return `must match ${DIRECTORY_PATTERN}`;
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
function validatePackageName(value) {
|
|
26
|
+
if (!value)
|
|
27
|
+
return "package name is required";
|
|
28
|
+
if (!PACKAGE_NAME_PATTERN.test(value))
|
|
29
|
+
return "invalid npm package name (e.g. @scope/name or just name)";
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function defaultPackageName(directory) {
|
|
33
|
+
// Most reasonable default: directory name as-is, no scope.
|
|
34
|
+
// User typically overrides with @org/name.
|
|
35
|
+
return directory;
|
|
36
|
+
}
|
|
37
|
+
async function ask(rl, question, options) {
|
|
38
|
+
const { defaultValue, validate } = options;
|
|
39
|
+
while (true) {
|
|
40
|
+
const prompt = defaultValue
|
|
41
|
+
? `? ${question} (${defaultValue}): `
|
|
42
|
+
: `? ${question}: `;
|
|
43
|
+
const raw = await rl.question(prompt);
|
|
44
|
+
const value = (raw.trim() || defaultValue || "").trim();
|
|
45
|
+
if (validate) {
|
|
46
|
+
const error = validate(value);
|
|
47
|
+
if (error) {
|
|
48
|
+
process.stdout.write(` ✗ ${error}\n`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function runPrompts(input_) {
|
|
56
|
+
// Resolve directory
|
|
57
|
+
let directory = input_.directory;
|
|
58
|
+
if (directory) {
|
|
59
|
+
const error = validateDirectory(directory);
|
|
60
|
+
if (error) {
|
|
61
|
+
throw new Error(`Invalid directory '${directory}': ${error}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Resolve package name
|
|
65
|
+
let packageName = input_.packageName;
|
|
66
|
+
if (packageName) {
|
|
67
|
+
const error = validatePackageName(packageName);
|
|
68
|
+
if (error) {
|
|
69
|
+
throw new Error(`Invalid package name '${packageName}': ${error}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// skipPrompts (--yes) requires both to be pre-supplied or defaulted
|
|
73
|
+
if (input_.skipPrompts) {
|
|
74
|
+
if (!directory) {
|
|
75
|
+
throw new Error("--yes requires <directory> to be passed as positional argument");
|
|
76
|
+
}
|
|
77
|
+
if (!packageName) {
|
|
78
|
+
packageName = defaultPackageName(directory);
|
|
79
|
+
}
|
|
80
|
+
return { directory, packageName };
|
|
81
|
+
}
|
|
82
|
+
// Interactive mode
|
|
83
|
+
const rl = createInterface({ input, output });
|
|
84
|
+
try {
|
|
85
|
+
if (!directory) {
|
|
86
|
+
directory = await ask(rl, "Directory name", {
|
|
87
|
+
validate: validateDirectory,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (!packageName) {
|
|
91
|
+
packageName = await ask(rl, "Package name", {
|
|
92
|
+
defaultValue: defaultPackageName(directory),
|
|
93
|
+
validate: validatePackageName,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
rl.close();
|
|
99
|
+
}
|
|
100
|
+
return { directory, packageName };
|
|
101
|
+
}
|
|
102
|
+
export async function confirm(question, defaultYes) {
|
|
103
|
+
const rl = createInterface({ input, output });
|
|
104
|
+
try {
|
|
105
|
+
const def = defaultYes ? "Y/n" : "y/N";
|
|
106
|
+
const raw = await rl.question(`? ${question} [${def}]: `);
|
|
107
|
+
const value = raw.trim().toLowerCase();
|
|
108
|
+
if (!value)
|
|
109
|
+
return defaultYes;
|
|
110
|
+
return value.startsWith("y");
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
rl.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hedwigjs/create-registry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Initializer for a Hedwig topics-registry package. Scaffolds a standalone TypeScript workspace with codegen, types, and helpers — no runtime dependency on @hedwigjs/*.",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"create-registry": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/hedwigjs/hedwig.git",
|
|
16
|
+
"directory": "packages/create-registry"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/hedwigjs/hedwig/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/hedwigjs/hedwig/tree/main/packages/create-registry#readme",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"templates",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "rm -rf dist && tsc",
|
|
30
|
+
"dev": "tsc --watch",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"hedwig",
|
|
35
|
+
"topics",
|
|
36
|
+
"registry",
|
|
37
|
+
"initializer",
|
|
38
|
+
"scaffold",
|
|
39
|
+
"create-package"
|
|
40
|
+
],
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20.0.0",
|
|
43
|
+
"typescript": "^5.8.3"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.17.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
Реестр топиков для `@hedwigjs/broker`. Создан через initializer `@hedwigjs/create-registry`.
|
|
4
|
+
|
|
5
|
+
## Структура
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/
|
|
9
|
+
├── domains/<domain>/<action>.v<N>.ts ← события, один файл = одно событие
|
|
10
|
+
├── lib/contract.ts ← тип EventContract (не трогать)
|
|
11
|
+
├── index.ts ← публичный entry (не трогать)
|
|
12
|
+
└── index.generated.ts ← агрегат, генерится автоматически
|
|
13
|
+
scripts/
|
|
14
|
+
└── build.mjs ← codegen
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Скрипты
|
|
18
|
+
|
|
19
|
+
| Команда | Что делает |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `npm run dev` | Watch-сборка во время разработки |
|
|
22
|
+
| `npm run build` | Одноразовая сборка перед публикацией |
|
|
23
|
+
|
|
24
|
+
## Добавление события
|
|
25
|
+
|
|
26
|
+
1. Скопировать соседний event-файл (или создать новый по шаблону ниже) в `src/domains/<domain>/<action>.v1.ts`:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import type { EventContract } from "../../lib/contract";
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
name: "<domain>.<action>.v1",
|
|
33
|
+
description: "Описание события",
|
|
34
|
+
|
|
35
|
+
payload: {} as {
|
|
36
|
+
// ...
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
examples: {
|
|
40
|
+
happy: { /* ... */ },
|
|
41
|
+
},
|
|
42
|
+
} satisfies EventContract;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. Поменять `name`, `description`, `payload`, `examples`
|
|
46
|
+
3. `npm run build` (или watch автоматом подхватит)
|
|
47
|
+
|
|
48
|
+
Конвенция: имя домена и action в `name` **обязательно** должны совпадать с путём — `users/fetched.v1.ts` → `name: "users.fetched.v1"`. Codegen упадёт с ошибкой при расхождении.
|
|
49
|
+
|
|
50
|
+
## Версионирование
|
|
51
|
+
|
|
52
|
+
При изменении контракта опубликованного события:
|
|
53
|
+
|
|
54
|
+
1. Скопировать `<action>.v1.ts` → `<action>.v2.ts`
|
|
55
|
+
2. Поменять `name` на `...v2`
|
|
56
|
+
3. В `<action>.v1.ts` добавить `deprecatedBy: "<domain>.<action>.v2"`
|
|
57
|
+
4. `npm run build`
|
|
58
|
+
|
|
59
|
+
## Использование в MFE
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import UsersFetched from "{{PACKAGE_NAME}}/domains/users/fetched.v1";
|
|
63
|
+
|
|
64
|
+
client.emit(UsersFetched.name, {
|
|
65
|
+
users: [...],
|
|
66
|
+
fetchedAt: Date.now(),
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Использование всего реестра (DevTools, типы брокера)
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { initBroker } from "@hedwigjs/broker";
|
|
74
|
+
import type { Topic, TopicPayloads } from "{{PACKAGE_NAME}}";
|
|
75
|
+
|
|
76
|
+
initBroker<Topic, TopicPayloads>({...});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { getBroker } from "@hedwigjs/broker";
|
|
81
|
+
import { MessageBrokerDevTools } from "@hedwigjs/devtools";
|
|
82
|
+
import { registry } from "{{PACKAGE_NAME}}";
|
|
83
|
+
|
|
84
|
+
<MessageBrokerDevTools broker={getBroker()} registry={registry} />
|
|
85
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PACKAGE_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./domains/*": {
|
|
13
|
+
"import": "./dist/domains/*.js",
|
|
14
|
+
"types": "./dist/domains/*.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": ["dist"],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "node scripts/build.mjs && tsc",
|
|
20
|
+
"dev": "node scripts/build.mjs --watch & tsc --watch",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.3.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/build.mjs — codegen реестра топиков.
|
|
4
|
+
*
|
|
5
|
+
* Сканирует src/domains/<domain>/<action>.v<N>.ts, валидирует и пишет
|
|
6
|
+
* src/index.generated.ts.
|
|
7
|
+
*
|
|
8
|
+
* Контракт пути:
|
|
9
|
+
* src/domains/<domain>/<action>.v<N>.ts
|
|
10
|
+
* где <domain> и <action> — kebab-case (^[a-z][a-z0-9-]*$),
|
|
11
|
+
* <N> — целое число.
|
|
12
|
+
*
|
|
13
|
+
* Топик строится как: <domain>.<action>.v<N>
|
|
14
|
+
*
|
|
15
|
+
* Валидация:
|
|
16
|
+
* - Глубина вложенности ровно 1 (домен в src/domains/, файл внутри)
|
|
17
|
+
* - Имя файла соответствует паттерну
|
|
18
|
+
* - Поле name внутри файла совпадает с derived-именем из пути
|
|
19
|
+
* - Нет дублей name
|
|
20
|
+
*
|
|
21
|
+
* Флаги:
|
|
22
|
+
* --watch — наблюдать за src/domains/, инкрементально пересобирать
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
|
|
26
|
+
import { existsSync, watch } from "node:fs";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
|
|
29
|
+
const ROOT = process.cwd();
|
|
30
|
+
const DOMAINS_DIR = join(ROOT, "src/domains");
|
|
31
|
+
const OUTPUT_FILE = join(ROOT, "src/index.generated.ts");
|
|
32
|
+
|
|
33
|
+
const DOMAIN_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
34
|
+
const FILE_NAME_PATTERN = /^([a-z][a-z0-9-]*)\.v(\d+)\.ts$/;
|
|
35
|
+
|
|
36
|
+
// ────────────────────────────────────────────────────────────────────
|
|
37
|
+
// File discovery
|
|
38
|
+
// ────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
async function listEventFiles() {
|
|
41
|
+
if (!existsSync(DOMAINS_DIR)) return [];
|
|
42
|
+
|
|
43
|
+
const files = [];
|
|
44
|
+
const domainEntries = await readdir(DOMAINS_DIR, { withFileTypes: true });
|
|
45
|
+
|
|
46
|
+
for (const domainEntry of domainEntries) {
|
|
47
|
+
if (!domainEntry.isDirectory()) continue;
|
|
48
|
+
if (domainEntry.name.startsWith(".")) continue;
|
|
49
|
+
|
|
50
|
+
const domainDir = join(DOMAINS_DIR, domainEntry.name);
|
|
51
|
+
const fileEntries = await readdir(domainDir, { withFileTypes: true });
|
|
52
|
+
|
|
53
|
+
for (const fileEntry of fileEntries) {
|
|
54
|
+
if (!fileEntry.isFile()) continue;
|
|
55
|
+
if (!fileEntry.name.endsWith(".ts")) continue;
|
|
56
|
+
files.push({
|
|
57
|
+
absolutePath: join(domainDir, fileEntry.name),
|
|
58
|
+
domain: domainEntry.name,
|
|
59
|
+
fileName: fileEntry.name,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ────────────────────────────────────────────────────────────────────
|
|
68
|
+
// Path → contract derivation
|
|
69
|
+
// ────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function pascalCase(parts) {
|
|
72
|
+
return parts
|
|
73
|
+
.flatMap((p) => p.split(/[-]/))
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())
|
|
76
|
+
.join("");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function deriveContract(file) {
|
|
80
|
+
if (!DOMAIN_PATTERN.test(file.domain)) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Invalid domain '${file.domain}' in src/domains/${file.domain}/${file.fileName}\n` +
|
|
83
|
+
` Domain must match ${DOMAIN_PATTERN}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const match = file.fileName.match(FILE_NAME_PATTERN);
|
|
88
|
+
if (!match) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Invalid file name 'src/domains/${file.domain}/${file.fileName}'\n` +
|
|
91
|
+
` Expected: <action>.v<N>.ts (kebab-case action, integer version)`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const [, action, version] = match;
|
|
96
|
+
const topic = `${file.domain}.${action}.v${version}`;
|
|
97
|
+
const identifier = pascalCase([file.domain, action, `V${version}`]);
|
|
98
|
+
const importPath = `./domains/${file.domain}/${action}.v${version}`;
|
|
99
|
+
const topicsKey = topic.replace(/[.\-]/g, "_").toUpperCase();
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
relPath: `${file.domain}/${file.fileName}`,
|
|
103
|
+
absolutePath: file.absolutePath,
|
|
104
|
+
domain: file.domain,
|
|
105
|
+
action,
|
|
106
|
+
version,
|
|
107
|
+
topic,
|
|
108
|
+
identifier,
|
|
109
|
+
importPath,
|
|
110
|
+
topicsKey,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ────────────────────────────────────────────────────────────────────
|
|
115
|
+
// Validation
|
|
116
|
+
// ────────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
async function validateNameMatchesPath(contract) {
|
|
119
|
+
const content = await readFile(contract.absolutePath, "utf-8");
|
|
120
|
+
const nameMatch = content.match(/name\s*:\s*["']([^"']+)["']/);
|
|
121
|
+
|
|
122
|
+
if (!nameMatch) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`src/domains/${contract.relPath}: cannot find 'name' field.\n` +
|
|
125
|
+
` Expected: name: "${contract.topic}",`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const declared = nameMatch[1];
|
|
130
|
+
if (declared !== contract.topic) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`src/domains/${contract.relPath}: name mismatch.\n` +
|
|
133
|
+
` Declared: "${declared}"\n` +
|
|
134
|
+
` Expected (from path): "${contract.topic}"`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function detectDuplicates(contracts) {
|
|
140
|
+
const seen = new Map();
|
|
141
|
+
for (const c of contracts) {
|
|
142
|
+
const previous = seen.get(c.topic);
|
|
143
|
+
if (previous) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Duplicate topic '${c.topic}':\n` +
|
|
146
|
+
` - src/domains/${previous.relPath}\n` +
|
|
147
|
+
` - src/domains/${c.relPath}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
seen.set(c.topic, c);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function detectDuplicateIdentifiers(contracts) {
|
|
155
|
+
const seen = new Map();
|
|
156
|
+
for (const c of contracts) {
|
|
157
|
+
const previous = seen.get(c.identifier);
|
|
158
|
+
if (previous) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Duplicate identifier '${c.identifier}' in generated registry:\n` +
|
|
161
|
+
` - src/domains/${previous.relPath} → ${previous.topic}\n` +
|
|
162
|
+
` - src/domains/${c.relPath} → ${c.topic}\n` +
|
|
163
|
+
` Rename one of the actions to disambiguate.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
seen.set(c.identifier, c);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ────────────────────────────────────────────────────────────────────
|
|
171
|
+
// Output rendering
|
|
172
|
+
// ────────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
const HEADER = `// AUTO-GENERATED. DO NOT EDIT.
|
|
175
|
+
// Run \`npm run build\` to regenerate.
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
function renderEmpty() {
|
|
179
|
+
return `${HEADER}
|
|
180
|
+
export const registry = {} as const;
|
|
181
|
+
export type Topic = keyof typeof registry;
|
|
182
|
+
export type TopicPayloads = {};
|
|
183
|
+
export const TOPICS = {} as const;
|
|
184
|
+
`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderRegistry(contracts) {
|
|
188
|
+
contracts.sort((a, b) => a.topic.localeCompare(b.topic));
|
|
189
|
+
|
|
190
|
+
const imports = contracts
|
|
191
|
+
.map((c) => `import ${c.identifier} from "${c.importPath}";`)
|
|
192
|
+
.join("\n");
|
|
193
|
+
|
|
194
|
+
const registryEntries = contracts
|
|
195
|
+
.map((c) => ` "${c.topic}": ${c.identifier},`)
|
|
196
|
+
.join("\n");
|
|
197
|
+
|
|
198
|
+
const topicsEntries = contracts
|
|
199
|
+
.map((c) => ` ${c.topicsKey}: "${c.topic}",`)
|
|
200
|
+
.join("\n");
|
|
201
|
+
|
|
202
|
+
return `${HEADER}
|
|
203
|
+
${imports}
|
|
204
|
+
|
|
205
|
+
export const registry = {
|
|
206
|
+
${registryEntries}
|
|
207
|
+
} as const;
|
|
208
|
+
|
|
209
|
+
export type Topic = keyof typeof registry;
|
|
210
|
+
|
|
211
|
+
export type TopicPayloads = {
|
|
212
|
+
[K in Topic]: (typeof registry)[K] extends { payload: infer P } ? P : never;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const TOPICS = {
|
|
216
|
+
${topicsEntries}
|
|
217
|
+
} as const;
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ────────────────────────────────────────────────────────────────────
|
|
222
|
+
// Main build
|
|
223
|
+
// ────────────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
async function build() {
|
|
226
|
+
const files = await listEventFiles();
|
|
227
|
+
const contracts = files.map(deriveContract);
|
|
228
|
+
|
|
229
|
+
for (const c of contracts) {
|
|
230
|
+
await validateNameMatchesPath(c);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
detectDuplicates(contracts);
|
|
234
|
+
detectDuplicateIdentifiers(contracts);
|
|
235
|
+
|
|
236
|
+
const output = contracts.length === 0 ? renderEmpty() : renderRegistry(contracts);
|
|
237
|
+
|
|
238
|
+
await mkdir(dirname(OUTPUT_FILE), { recursive: true });
|
|
239
|
+
await writeFile(OUTPUT_FILE, output, "utf-8");
|
|
240
|
+
|
|
241
|
+
const count = contracts.length;
|
|
242
|
+
const noun = count === 1 ? "topic" : "topics";
|
|
243
|
+
console.log(`✔ Generated src/index.generated.ts (${count} ${noun})`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function buildSafe(throwOnError) {
|
|
247
|
+
try {
|
|
248
|
+
await build();
|
|
249
|
+
return true;
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.error(`✗ Build failed:\n ${err.message}`);
|
|
252
|
+
if (throwOnError) throw err;
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ────────────────────────────────────────────────────────────────────
|
|
258
|
+
// CLI entry
|
|
259
|
+
// ────────────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
const watchMode = process.argv.includes("--watch");
|
|
262
|
+
|
|
263
|
+
if (watchMode) {
|
|
264
|
+
await buildSafe(false);
|
|
265
|
+
console.log(`Watching ${DOMAINS_DIR} for changes...`);
|
|
266
|
+
|
|
267
|
+
if (!existsSync(DOMAINS_DIR)) {
|
|
268
|
+
await mkdir(DOMAINS_DIR, { recursive: true });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let timer = null;
|
|
272
|
+
const debouncedRebuild = () => {
|
|
273
|
+
if (timer) clearTimeout(timer);
|
|
274
|
+
timer = setTimeout(() => {
|
|
275
|
+
console.log("Change detected, rebuilding...");
|
|
276
|
+
buildSafe(false);
|
|
277
|
+
}, 100);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
watch(DOMAINS_DIR, { recursive: true }, debouncedRebuild);
|
|
281
|
+
} else {
|
|
282
|
+
const ok = await buildSafe(false);
|
|
283
|
+
if (!ok) process.exit(1);
|
|
284
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Форма контракта одного события.
|
|
3
|
+
* Используется через `satisfies` в файлах domains/.
|
|
4
|
+
*
|
|
5
|
+
* Этот файл копируется initializer'ом в org-package при создании реестра.
|
|
6
|
+
* После copy редактирование не предполагается.
|
|
7
|
+
*/
|
|
8
|
+
export interface EventContract<
|
|
9
|
+
TName extends string = string,
|
|
10
|
+
TPayload = unknown,
|
|
11
|
+
> {
|
|
12
|
+
/** Имя топика, формат: <domain>.<action>.v<N> */
|
|
13
|
+
name: TName;
|
|
14
|
+
|
|
15
|
+
/** Описание для DevTools и команды */
|
|
16
|
+
description: string;
|
|
17
|
+
|
|
18
|
+
/** Тип payload — объявляется через `as { ... }` */
|
|
19
|
+
payload: TPayload;
|
|
20
|
+
|
|
21
|
+
/** Фикстуры для тестов и DevTools-имитации. Минимум — ключ `happy` */
|
|
22
|
+
examples: Record<string, TPayload>;
|
|
23
|
+
|
|
24
|
+
/** Если событие deprecated — указатель на новую версию */
|
|
25
|
+
deprecatedBy?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Отмечает событие как чисто телеметрическое (трейс, TTFB-hint и т.п.):
|
|
29
|
+
* по замыслу у него может не быть business-подписчиков, и
|
|
30
|
+
* `NACK NO_SUBSCRIBERS` для него — ожидаемое состояние, а не ошибка.
|
|
31
|
+
*
|
|
32
|
+
* DevTools использует этот флаг чтобы рендерить такие NACK'и
|
|
33
|
+
* нейтрально (не красным) и метить строку бейджем `trace`.
|
|
34
|
+
*/
|
|
35
|
+
observability?: boolean;
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"resolveJsonModule": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"]
|
|
18
|
+
}
|