@carlos-tzin/tzin 1.0.3 → 1.0.5

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/CHANGELOG.md CHANGED
@@ -1,6 +1,57 @@
1
1
  # Changelog
2
2
 
3
- ## 1.0.3Quality pass before wider adoption
3
+ ## 1.0.52026-09-21
4
+
5
+ Deployment polish + release automation. No API changes to the framework core.
6
+
7
+ ### CLI deploy
8
+
9
+ - `tzin deploy --target node --pack` builds with `tsc` and packs
10
+ `dist + package.json (+Dockerfile / docker-compose.yml / .dockerignore`
11
+ when present) into `<name>-<version>.tgz`
12
+ - `tzin deploy --target node --docker <tag> [--push]` runs
13
+ `docker build -t <tag>` and optionally `docker push`
14
+ - Fix: the trailing `usage()` call and a premature `process.exit(0)` killed
15
+ async deploy/build before the `tsc` child finished — deploy printed the
16
+ help text and died silently. Now gated behind a `matched` flag, unknown
17
+ targets error clearly (`Available: node, workers`)
18
+
19
+ ### Docker + docs
20
+
21
+ - `Dockerfile` (multi-stage, `node:20-slim`, prod-only deps,
22
+ `HEALTHCHECK` on `/health`) aligned to the real node template layout
23
+ (`tsconfig.json` → `dist/index.js`); `.dockerignore`; `docker-compose.yml`
24
+ (app + Redis with healthchecks)
25
+ - `docs/deployment.md`: Node container, Workers Durable Objects,
26
+ multi-node Redis recipes, exact `create-tzin → docker build` flow
27
+ - `examples/workers-quickstart/` + `.md`: copy-paste wrangler project
28
+ (DO + channels + presence); README examples table updated
29
+
30
+ ### Templates
31
+
32
+ - All `create-tzin` templates bumped to `@carlos-tzin/tzin ^1.0.5`
33
+ - Fix: `createApp([…, usersRoute])` nested an array inside the route list —
34
+ now `...usersRoute` in node/bun/workers templates
35
+ - New optional `src/bus.ts` in the node template: `redisBus()` returns
36
+ `undefined` without `REDIS_URL`, ioredis-backed `MessageBus` with it
37
+ (lazy import, zero new deps to typecheck)
38
+
39
+ ### Release process
40
+
41
+ - `scripts/release.mjs <patch|minor|major> [--dry-run]`: version bump,
42
+ template sync, CHANGELOG skeleton, commit + tag
43
+ - `.github/workflows/release.yml`: tag-triggered npm publish via
44
+ Trusted Publishing (OIDC, `--provenance`), no `NPM_TOKEN` secret
45
+
46
+ ## 1.0.4 — Node 20 compatibility fix
47
+
48
+ No API changes. CI was failing on the Node 20 floor because the WS test and the
49
+ Workers probe relied on the global `WebSocket`, which only exists on Node ≥22.
50
+
51
+ ### Testing & coverage
52
+
53
+ - `native websockets` test and the Workers probe use the `ws` package client
54
+ instead of the global `WebSocket`, keeping the Node 20 floor green
4
55
 
5
56
  No API changes. Focused on proving the stable claim in CI and on the
6
57
  type surface.
package/README.md CHANGED
@@ -27,6 +27,7 @@ npx create-tzin my-app
27
27
 
28
28
  - **[API Reference](docs/api-reference.md)** — all exports, types, and options
29
29
  - **[Architecture Guide](docs/architecture.md)** — request pipeline, design decisions, internals
30
+ - **[Deployment Guide](docs/deployment.md)** — Docker, Cloudflare Workers, multi-node clustering
30
31
  - **[Roadmap](docs/roadmap.md)** — future features
31
32
 
32
33
  ## Why another framework?
@@ -209,12 +210,12 @@ npx tzin build # production build
209
210
  ## CLI
210
211
 
211
212
  ```bash
212
- tzin dev [--port N] # dev server with hot reload
213
- tzin build # build for production
214
- tzin deploy --target node|workers # deploy
215
- tzin generate route <name> # scaffold a route
216
- tzin generate middleware <name> # scaffold middleware
217
- tzin generate test <name> # scaffold a test
213
+ tzin dev [--port N] # dev server with hot reload
214
+ tzin build # build for production
215
+ tzin deploy --target node|workers [--pack] [--docker <tag> [--push]] # deploy
216
+ tzin generate route <name> # scaffold a route
217
+ tzin generate middleware <name> # scaffold middleware
218
+ tzin generate test <name> # scaffold a test
218
219
  ```
219
220
 
220
221
  ## Benchmarks
@@ -247,6 +248,8 @@ tzin grows **strictly linearly** (~130 types/endpoint).
247
248
  | `examples/todo-api.ts` | Node | Full CRUD: auth middleware, DI, OpenAPI, MCP |
248
249
  | `examples/mcp-demo.ts` | Node | MCP server over stdio |
249
250
  | `examples/ws-demo.ts` | Bun | WebSocket channels with presence |
251
+ | `examples/workers-quickstart/` | Workers | Copy-paste wrangler project: DO + channels + presence |
252
+ | `examples/workers-quickstart.md` | Workers | Same project, explained file-by-file |
250
253
 
251
254
  ## Design principles
252
255
 
package/dist/cli.js CHANGED
@@ -1,15 +1,17 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { resolve } from 'node:path';
4
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
4
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
5
5
  const [, , cmd, ...rest] = process.argv;
6
+ let matched = false;
6
7
  function usage() {
7
8
  console.error(`tzin CLI
8
9
 
9
10
  Commands:
10
11
  tzin dev [entry] [--port N] start dev server with hot reload
11
12
  tzin build build for production
12
- tzin deploy [--target <target>] deploy (node, workers)
13
+ tzin deploy [--target <target>] [--pack] [--docker <tag> [--push]]
14
+ deploy (node, workers)
13
15
  tzin generate route <name> generate a route stub
14
16
  tzin generate middleware <name> generate a middleware stub
15
17
  tzin generate test <name> generate a test stub`);
@@ -19,6 +21,7 @@ if (!cmd || cmd === '--help' || cmd === '-h')
19
21
  usage();
20
22
  // ── dev ──────────────────────────────────────────────────────────────
21
23
  if (cmd === 'dev') {
24
+ matched = true;
22
25
  let entry = '';
23
26
  let port = '3000';
24
27
  const portFlag = rest.indexOf('--port');
@@ -39,6 +42,7 @@ if (cmd === 'dev') {
39
42
  }
40
43
  // ── build ─────────────────────────────────────────────────────────────
41
44
  if (cmd === 'build') {
45
+ matched = true;
42
46
  const cwd = process.cwd();
43
47
  // Check for tsconfig
44
48
  if (!existsSync(resolve(cwd, 'tsconfig.json'))) {
@@ -65,9 +69,16 @@ if (cmd === 'build') {
65
69
  }
66
70
  // ── deploy ────────────────────────────────────────────────────────────
67
71
  if (cmd === 'deploy') {
72
+ matched = true;
68
73
  const cwd = process.cwd();
69
74
  const targetFlag = rest.indexOf('--target');
70
75
  const target = targetFlag !== -1 ? rest[targetFlag + 1] : 'node';
76
+ const pack = rest.includes('--pack');
77
+ const dockerIdx = rest.indexOf('--docker');
78
+ const dockerTag = dockerIdx !== -1 && rest[dockerIdx + 1] && !rest[dockerIdx + 1].startsWith('-')
79
+ ? rest[dockerIdx + 1]
80
+ : null;
81
+ const push = rest.includes('--push');
71
82
  if (target === 'workers') {
72
83
  console.log('Deploying to Cloudflare Workers...');
73
84
  // Check for wrangler.toml
@@ -83,6 +94,16 @@ if (cmd === 'deploy') {
83
94
  process.exit(0);
84
95
  }
85
96
  // Default: Node
97
+ if (target !== 'node') {
98
+ console.error(`Unknown deploy target: ${target}`);
99
+ console.error('Available: node, workers');
100
+ process.exit(1);
101
+ }
102
+ // Check for tsconfig
103
+ if (!existsSync(resolve(cwd, 'tsconfig.json'))) {
104
+ console.error('No tsconfig.json found');
105
+ process.exit(1);
106
+ }
86
107
  console.log('Building for production...');
87
108
  const build = spawn('npx', ['tsc', '-p', 'tsconfig.json'], {
88
109
  cwd,
@@ -92,13 +113,83 @@ if (cmd === 'deploy') {
92
113
  if (code !== 0)
93
114
  process.exit(code ?? 1);
94
115
  console.log('\n✓ Build complete');
116
+ afterBuild();
117
+ });
118
+ // NOTE: no trailing process.exit(0) here — the parent must stay alive
119
+ // until the build child exits and afterBuild() runs. Calling
120
+ // process.exit() here would kill the process (and the child listener)
121
+ // before the build finishes.
122
+ function afterBuild() {
123
+ const pkgPath = resolve(cwd, 'package.json');
124
+ let name = 'app';
125
+ let version = '0.0.0';
126
+ try {
127
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
128
+ name = String(pkg.name ?? 'app').replace(/^@/, '').replace(/\//g, '-');
129
+ version = String(pkg.version ?? '0.0.0');
130
+ }
131
+ catch {
132
+ console.error('No package.json found — is this a tzin project?');
133
+ process.exit(1);
134
+ }
135
+ // --pack: runnable tarball (dist + manifest + container files when present).
136
+ if (pack) {
137
+ const out = resolve(cwd, `${name}-${version}.tgz`);
138
+ const files = ['dist', 'package.json'];
139
+ for (const f of ['Dockerfile', 'docker-compose.yml', '.dockerignore']) {
140
+ if (existsSync(resolve(cwd, f)))
141
+ files.push(f);
142
+ }
143
+ console.log(`Packing ${files.join(', ')} → ${out}`);
144
+ const tar = spawn('tar', ['-czf', out, ...files], { cwd, stdio: 'inherit' });
145
+ tar.on('exit', (tarCode) => {
146
+ if (tarCode !== 0)
147
+ process.exit(tarCode ?? 1);
148
+ console.log(`\n✓ Packed ${out}`);
149
+ console.log(`Unpack + run: tar -xzf ${name}-${version}.tgz && node dist/index.js`);
150
+ if (dockerTag)
151
+ dockerBuild();
152
+ else
153
+ process.exit(0);
154
+ });
155
+ return;
156
+ }
157
+ if (dockerTag) {
158
+ dockerBuild();
159
+ return;
160
+ }
95
161
  console.log('Run: node dist/index.js');
162
+ console.log('Tip: tzin deploy --target node --pack [--docker <tag> [--push]] for a runnable artifact');
96
163
  process.exit(0);
97
- });
98
- process.exit(0);
164
+ }
165
+ function dockerBuild() {
166
+ if (!dockerTag)
167
+ return;
168
+ if (!existsSync(resolve(cwd, 'Dockerfile'))) {
169
+ console.error('No Dockerfile found. Copy the reference Dockerfile from the tzin repo (see docs/deployment.md).');
170
+ process.exit(1);
171
+ }
172
+ console.log(`Building Docker image ${dockerTag}...`);
173
+ const args = ['build', '-t', dockerTag, '.'];
174
+ const child = spawn('docker', args, { cwd, stdio: 'inherit' });
175
+ child.on('exit', (buildCode) => {
176
+ if (buildCode !== 0)
177
+ process.exit(buildCode ?? 1);
178
+ console.log(`\n✓ Image built: ${dockerTag}`);
179
+ if (!push) {
180
+ console.log(`Run: docker run -p 3000:3000 --env PORT=3000 ${dockerTag}`);
181
+ process.exit(0);
182
+ return;
183
+ }
184
+ console.log(`Pushing ${dockerTag}...`);
185
+ const pushChild = spawn('docker', ['push', dockerTag], { cwd, stdio: 'inherit' });
186
+ pushChild.on('exit', (pushCode) => process.exit(pushCode ?? 1));
187
+ });
188
+ }
99
189
  }
100
190
  // ── generate ─────────────────────────────────────────────────────────
101
191
  if (cmd === 'generate' || cmd === 'g') {
192
+ matched = true;
102
193
  const [sub, ...args] = rest;
103
194
  const name = args[0];
104
195
  if (!sub || !name) {
@@ -184,4 +275,5 @@ describe('${name}', () => {
184
275
  console.error('Available: route, middleware, test');
185
276
  process.exit(1);
186
277
  }
187
- usage();
278
+ if (!matched)
279
+ usage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlos-tzin/tzin",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Contract-first TypeScript framework. Types that scale, realtime channels with presence, and an MCP server for every API.",
5
5
  "license": "MIT",
6
6
  "author": "The tzin authors",