@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 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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"}
@@ -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,3 @@
1
+ node_modules
2
+ dist
3
+ src/index.generated.ts
@@ -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,7 @@
1
+ // AUTO-GENERATED. DO NOT EDIT.
2
+ // Run `npm run build` to regenerate.
3
+
4
+ export const registry = {} as const;
5
+ export type Topic = keyof typeof registry;
6
+ export type TopicPayloads = {};
7
+ export const TOPICS = {} as const;
@@ -0,0 +1,3 @@
1
+ // Публичный entry org-package'а. Не редактируется.
2
+ // Реэкспортирует всё, что генерит scripts/build.mjs.
3
+ export * from "./index.generated";
@@ -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
+ }