@carlos-tzin/tzin 1.0.4 → 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 +43 -0
- package/README.md +9 -6
- package/dist/cli.js +97 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.5 — 2026-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
|
+
|
|
3
46
|
## 1.0.4 — Node 20 compatibility fix
|
|
4
47
|
|
|
5
48
|
No API changes. CI was failing on the Node 20 floor because the WS test and the
|
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]
|
|
213
|
-
tzin build
|
|
214
|
-
tzin deploy --target node|workers # deploy
|
|
215
|
-
tzin generate route <name>
|
|
216
|
-
tzin generate middleware <name>
|
|
217
|
-
tzin generate test <name>
|
|
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>]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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",
|