@carlos-tzin/tzin 1.0.6 → 1.0.8
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 +102 -0
- package/README.md +12 -2
- package/dist/auth.js +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +139 -98
- package/dist/config.d.ts +6 -0
- package/dist/config.js +42 -16
- package/dist/dev-server.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,107 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.8 — 2026-10-01
|
|
4
|
+
|
|
5
|
+
- **`tzin.config.ts` was silently ignored.** `loadConfig()` bailed out with
|
|
6
|
+
`null` for `.ts` files, so the `tzin.config.ts` that every scaffolded project
|
|
7
|
+
ships never applied — changing `port` there did nothing while the dev server
|
|
8
|
+
kept listening on the default. Added `loadConfigAsync()`, which reads a
|
|
9
|
+
TypeScript config through a dynamic import, and wired the dev server (which
|
|
10
|
+
runs under `tsx`) to it. The sync `loadConfig()` keeps its signature and
|
|
11
|
+
still handles `.js` / `.mjs` / `.json`.
|
|
12
|
+
- Tests: `tzin.config.ts` is now covered end to end (the dev server is spawned
|
|
13
|
+
and asserted to serve on the configured port).
|
|
14
|
+
- `vitest.config.ts`: exclude `.kilo/` and `.kilocode/`. Agent tooling leaves
|
|
15
|
+
git worktrees inside the repo holding stale copies of the suite, which the
|
|
16
|
+
default glob picked up (9 test files became 17).
|
|
17
|
+
|
|
18
|
+
## 1.0.7 — 2026-09-27
|
|
19
|
+
|
|
20
|
+
The `tzin` CLI was announced as a headline feature but was **not reachable** in
|
|
21
|
+
a published install. `npm run dev` in a freshly scaffolded app failed with
|
|
22
|
+
`sh: 1: tzin: not found`, and `npx tzin dev` in the docs 404'd because no
|
|
23
|
+
unscoped `tzin` package exists on npm. Fixed here.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **`bin` field added to `package.json`** — `@carlos-tzin/tzin` now installs a
|
|
28
|
+
`tzin` executable into `node_modules/.bin`, so the `create-tzin` node
|
|
29
|
+
template (`"dev": "tzin dev"`, `"build": "tzin build"`) works after
|
|
30
|
+
`npm install`.
|
|
31
|
+
- **Shebang in `src/cli.ts`** — `dist/cli.js` is now a runnable binary. `tsc`
|
|
32
|
+
preserves it on emit.
|
|
33
|
+
- **`dev` no longer resolves a `.ts` file from `dist/`** — the CLI pointed
|
|
34
|
+
`tsx watch` at `./dev-server.ts` relative to itself, which only exists in
|
|
35
|
+
`src/`. In the published package the watcher died with
|
|
36
|
+
`ERR_MODULE_NOT_FOUND: .../dist/dev-server.ts`. It now resolves the sibling
|
|
37
|
+
module for both layouts (`.js` when built, `.ts` when run from source).
|
|
38
|
+
- **`tzin build` / `tzin dev` no longer return before their child exits** —
|
|
39
|
+
both spawned `tsc` / `tsx watch` and then called `process.exit(0)`, so
|
|
40
|
+
`tzin build` reported success (exit 0) even with type errors and the dev
|
|
41
|
+
server was orphaned. The CLI now waits and forwards the child's exit code,
|
|
42
|
+
breaking a CI build on a type error instead of shipping stale output.
|
|
43
|
+
- **Tool resolution** — `tsx` / `tsc` / `wrangler` are now taken from the
|
|
44
|
+
project's own `node_modules/.bin` (walking up from cwd). The npx fallback
|
|
45
|
+
pins the right package (`--package typescript tsc`); previously `npx tsc`
|
|
46
|
+
without a local install ran an unrelated third-party `tsc` package.
|
|
47
|
+
|
|
48
|
+
### Also fixed (found while verifying the above — both shipped broken in 1.0.6)
|
|
49
|
+
|
|
50
|
+
- **`signJwt` / `verifyJwt` threw in every consumer install.**
|
|
51
|
+
`src/auth.ts` used a bare `require('node:crypto')` inside an ESM package,
|
|
52
|
+
so `dist/auth.js` shipped as `require` + top-level `await` and blew up with
|
|
53
|
+
`ReferenceError: Cannot determine intended module format` /
|
|
54
|
+
`ERR_AMBIGUOUS_MODULE_SYNTAX` on the first call. The unit tests passed
|
|
55
|
+
because Vitest's module runner injects a `require` shim, so nothing
|
|
56
|
+
exercised the published artifact. Now a static `import`.
|
|
57
|
+
- **`loadConfig()` silently ignored every config file.** Same bare `require`
|
|
58
|
+
in `src/config.ts`, swallowed by the surrounding `try/catch` and turned into
|
|
59
|
+
a `null` return, so `port` / `openapi` / `routes` from `tzin.config.*` never
|
|
60
|
+
applied. Now uses `createRequire(import.meta.url)`, which also lets Node load
|
|
61
|
+
ESM `.js` and `.mjs` configs.
|
|
62
|
+
|
|
63
|
+
### Docs
|
|
64
|
+
|
|
65
|
+
- Replaced every `npx tzin dev` / `npx tzin build` (which 404) with
|
|
66
|
+
`npm run dev` / `npm run build`, plus `npx --package @carlos-tzin/tzin tzin dev`
|
|
67
|
+
for one-off runs without a local install.
|
|
68
|
+
|
|
69
|
+
### Tests
|
|
70
|
+
|
|
71
|
+
- `test/cli.test.ts`: asserts the `bin` mapping, the shebang in source and
|
|
72
|
+
build output, that `build` waits for `tsc` and propagates a non-zero exit
|
|
73
|
+
code, and that `dev` from the built layout finds `dev-server` instead of
|
|
74
|
+
crashing.
|
|
75
|
+
- `test/cli.test.ts`: runs the built `dist/` in a plain node process to assert
|
|
76
|
+
JWT signing, `loadConfig`, and a served app work outside Vitest's transform
|
|
77
|
+
(the gap that let the `require` bugs reach npm).
|
|
78
|
+
|
|
79
|
+
## 1.0.6 — 2026-09-22
|
|
80
|
+
|
|
81
|
+
Deployment fixes after the 1.0.5 release. No API changes.
|
|
82
|
+
|
|
83
|
+
### Templates
|
|
84
|
+
|
|
85
|
+
- `create-tzin/templates/node/src/bus.ts`: `import('ioredis' as string)`
|
|
86
|
+
— the dynamic import was being resolved by `tsc` at compile time, so a
|
|
87
|
+
freshly scaffolded app (without `ioredis` installed) failed to build with
|
|
88
|
+
`Cannot find module 'ioredis'`. Now it typechecks and runs with zero new
|
|
89
|
+
dependencies; `ioredis` is only fetched at runtime when `REDIS_URL` is set.
|
|
90
|
+
|
|
91
|
+
### Docker
|
|
92
|
+
|
|
93
|
+
- `Dockerfile` HEALTHCHECK now hits `/openapi.json` instead of `/health`.
|
|
94
|
+
The node template mounts `bearerAuth` on every route, so `/health` returns
|
|
95
|
+
401 and the healthcheck would always fail. `/openapi.json` is public.
|
|
96
|
+
|
|
97
|
+
### Compose / docs
|
|
98
|
+
|
|
99
|
+
- `docker-compose.yml`: removed the `TZIN_BUS=redis` env var (the framework
|
|
100
|
+
never read it — clustering is enabled by `REDIS_URL` via `src/bus.ts`).
|
|
101
|
+
Added a comment that `build: .` points at the app root, not the framework
|
|
102
|
+
repo.
|
|
103
|
+
- `docs/deployment.md` env-var table: `TZIN_BUS` → `REDIS_URL`.
|
|
104
|
+
|
|
3
105
|
## 1.0.5 — 2026-09-21
|
|
4
106
|
|
|
5
107
|
Deployment polish + release automation. No API changes to the framework core.
|
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
[](https://github.com/Tzinny-dev/tzin/actions/workflows/ci.yml)
|
|
9
9
|
[](https://www.npmjs.com/package/@carlos-tzin/tzin)
|
|
10
10
|
[](https://paypal.me/carlostzin)
|
|
11
|
+
[](https://buymeacoffee.com/tzinny)
|
|
11
12
|
|
|
12
13
|
**Status: stable (1.0.x).** The core works end-to-end (see [Benchmarks](#benchmarks));
|
|
13
14
|
1.0.0 shipped the stable API, and 1.0.2 adds test coverage gates, a wider CI
|
|
@@ -25,6 +26,8 @@ npx create-tzin my-app
|
|
|
25
26
|
|
|
26
27
|
## Documentation
|
|
27
28
|
|
|
29
|
+
Hosted site: **[tzin.tzinny.com](https://tzin.tzinny.com)**
|
|
30
|
+
|
|
28
31
|
- **[API Reference](docs/api-reference.md)** — all exports, types, and options
|
|
29
32
|
- **[Architecture Guide](docs/architecture.md)** — request pipeline, design decisions, internals
|
|
30
33
|
- **[Deployment Guide](docs/deployment.md)** — Docker, Cloudflare Workers, multi-node clustering
|
|
@@ -203,8 +206,15 @@ export const app = createApp([getUserRoute], {
|
|
|
203
206
|
### 4. Run
|
|
204
207
|
|
|
205
208
|
```bash
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
npm run dev # tzin dev -> dev server at http://localhost:3000
|
|
210
|
+
npm run build # tzin build -> production build
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The `tzin` binary ships with the package, so these scripts work after `npm install`. Without
|
|
214
|
+
a local install, run it through npm exec:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
npx --package @carlos-tzin/tzin tzin dev
|
|
208
218
|
```
|
|
209
219
|
|
|
210
220
|
## CLI
|
package/dist/auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
1
2
|
import { defineContext } from './context.js';
|
|
2
3
|
// ── Context Key ──────────────────────────────────────────────────────
|
|
3
4
|
/** Typed key for the authenticated user */
|
|
@@ -10,7 +11,6 @@ function base64UrlDecode(data) {
|
|
|
10
11
|
return Buffer.from(data, 'base64url').toString();
|
|
11
12
|
}
|
|
12
13
|
function hmacSign(data, secret) {
|
|
13
|
-
const { createHmac } = require('node:crypto');
|
|
14
14
|
return createHmac('sha256', secret).update(data).digest('base64url');
|
|
15
15
|
}
|
|
16
16
|
/**
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import { spawn } from 'node:child_process';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
|
+
import { resolve, dirname } from 'node:path';
|
|
4
5
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
5
6
|
const [, , cmd, ...rest] = process.argv;
|
|
7
|
+
// Published package ships dist/*.js, repo runs from src/*.ts — pick the one that exists.
|
|
8
|
+
function siblingModule(name) {
|
|
9
|
+
const js = fileURLToPath(new URL(`./${name}.js`, import.meta.url));
|
|
10
|
+
if (existsSync(js))
|
|
11
|
+
return js;
|
|
12
|
+
const ts = fileURLToPath(new URL(`./${name}.ts`, import.meta.url));
|
|
13
|
+
if (existsSync(ts))
|
|
14
|
+
return ts;
|
|
15
|
+
console.error(`tzin: cannot locate ${name} (looked for ${js} and ${ts})`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
// npm package that provides each binary (never assume bin name == package name:
|
|
19
|
+
// `npx tsc` without typescript installed runs an unrelated third-party `tsc`).
|
|
20
|
+
const TOOL_PACKAGE = { tsc: 'typescript' };
|
|
21
|
+
const isWin = process.platform === 'win32';
|
|
22
|
+
function localBin(name, cwd) {
|
|
23
|
+
const exe = isWin ? `${name}.cmd` : name;
|
|
24
|
+
let dir = cwd;
|
|
25
|
+
for (;;) {
|
|
26
|
+
const candidate = resolve(dir, 'node_modules', '.bin', exe);
|
|
27
|
+
if (existsSync(candidate))
|
|
28
|
+
return candidate;
|
|
29
|
+
const parent = dirname(dir);
|
|
30
|
+
if (parent === dir)
|
|
31
|
+
return null;
|
|
32
|
+
dir = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Prefer the project's own binary; only fall back to npx (pinned to the right
|
|
36
|
+
// package) when the tool is not installed locally.
|
|
37
|
+
function runTool(name, args, cwd) {
|
|
38
|
+
const local = localBin(name, cwd);
|
|
39
|
+
if (local)
|
|
40
|
+
return spawn(local, args, { cwd, stdio: 'inherit' });
|
|
41
|
+
const pkg = TOOL_PACKAGE[name] ?? name;
|
|
42
|
+
console.error(`tzin: ${name} not found in ${cwd} (install it to pin the version)`);
|
|
43
|
+
return spawn('npx', ['--yes', '--package', pkg, name, ...args], {
|
|
44
|
+
cwd,
|
|
45
|
+
stdio: 'inherit',
|
|
46
|
+
shell: isWin,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
6
49
|
let matched = false;
|
|
7
50
|
function usage() {
|
|
8
51
|
console.error(`tzin CLI
|
|
@@ -22,6 +65,7 @@ if (!cmd || cmd === '--help' || cmd === '-h')
|
|
|
22
65
|
// ── dev ──────────────────────────────────────────────────────────────
|
|
23
66
|
if (cmd === 'dev') {
|
|
24
67
|
matched = true;
|
|
68
|
+
const cwd = process.cwd();
|
|
25
69
|
let entry = '';
|
|
26
70
|
let port = '3000';
|
|
27
71
|
const portFlag = rest.indexOf('--port');
|
|
@@ -30,15 +74,17 @@ if (cmd === 'dev') {
|
|
|
30
74
|
const entryArg = rest.find((a) => !a.startsWith('-'));
|
|
31
75
|
if (entryArg)
|
|
32
76
|
entry = entryArg;
|
|
33
|
-
const devServer =
|
|
34
|
-
const args = ['
|
|
77
|
+
const devServer = siblingModule('dev-server');
|
|
78
|
+
const args = ['watch', '--clear-screen=false', devServer];
|
|
35
79
|
if (entry)
|
|
36
80
|
args.push(entry);
|
|
37
81
|
args.push('--port', port);
|
|
38
|
-
const child =
|
|
82
|
+
const child = runTool('tsx', args, cwd);
|
|
39
83
|
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
40
|
-
|
|
41
|
-
|
|
84
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
85
|
+
// Stay alive until the watcher exits — the parent must not return the
|
|
86
|
+
// prompt (or npm's exit code) while the dev server is still running.
|
|
87
|
+
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
42
88
|
}
|
|
43
89
|
// ── build ─────────────────────────────────────────────────────────────
|
|
44
90
|
if (cmd === 'build') {
|
|
@@ -55,17 +101,15 @@ if (cmd === 'build') {
|
|
|
55
101
|
process.exit(1);
|
|
56
102
|
}
|
|
57
103
|
console.log('Building...');
|
|
58
|
-
const child =
|
|
59
|
-
cwd,
|
|
60
|
-
stdio: 'inherit',
|
|
61
|
-
});
|
|
104
|
+
const child = runTool('tsc', ['-p', 'tsconfig.json'], cwd);
|
|
62
105
|
child.on('exit', (code) => {
|
|
63
106
|
if (code === 0) {
|
|
64
107
|
console.log('\n✓ Build complete → dist/');
|
|
65
108
|
}
|
|
66
109
|
process.exit(code ?? 1);
|
|
67
110
|
});
|
|
68
|
-
process.exit(0)
|
|
111
|
+
// NOTE: no trailing process.exit(0) — it would return success to the
|
|
112
|
+
// caller before tsc finishes (and mask type errors).
|
|
69
113
|
}
|
|
70
114
|
// ── deploy ────────────────────────────────────────────────────────────
|
|
71
115
|
if (cmd === 'deploy') {
|
|
@@ -86,105 +130,102 @@ if (cmd === 'deploy') {
|
|
|
86
130
|
console.error('No wrangler.toml found. Run: npx wrangler init');
|
|
87
131
|
process.exit(1);
|
|
88
132
|
}
|
|
89
|
-
const child =
|
|
90
|
-
cwd,
|
|
91
|
-
stdio: 'inherit',
|
|
92
|
-
});
|
|
133
|
+
const child = runTool('wrangler', ['deploy'], cwd);
|
|
93
134
|
child.on('exit', (code) => process.exit(code ?? 1));
|
|
94
|
-
process.exit(0);
|
|
95
135
|
}
|
|
96
|
-
|
|
97
|
-
|
|
136
|
+
else if (target === 'node') {
|
|
137
|
+
nodeDeploy();
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
98
140
|
console.error(`Unknown deploy target: ${target}`);
|
|
99
141
|
console.error('Available: node, workers');
|
|
100
142
|
process.exit(1);
|
|
101
143
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
console.log('Building for production...');
|
|
108
|
-
const build = spawn('npx', ['tsc', '-p', 'tsconfig.json'], {
|
|
109
|
-
cwd,
|
|
110
|
-
stdio: 'inherit',
|
|
111
|
-
});
|
|
112
|
-
build.on('exit', (code) => {
|
|
113
|
-
if (code !== 0)
|
|
114
|
-
process.exit(code ?? 1);
|
|
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?');
|
|
144
|
+
function nodeDeploy() {
|
|
145
|
+
// Check for tsconfig
|
|
146
|
+
if (!existsSync(resolve(cwd, 'tsconfig.json'))) {
|
|
147
|
+
console.error('No tsconfig.json found');
|
|
133
148
|
process.exit(1);
|
|
134
149
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
console.log('Building for production...');
|
|
151
|
+
const build = runTool('tsc', ['-p', 'tsconfig.json'], cwd);
|
|
152
|
+
build.on('exit', (code) => {
|
|
153
|
+
if (code !== 0)
|
|
154
|
+
process.exit(code ?? 1);
|
|
155
|
+
console.log('\n✓ Build complete');
|
|
156
|
+
afterBuild();
|
|
157
|
+
});
|
|
158
|
+
// NOTE: no trailing process.exit(0) here — the parent must stay alive
|
|
159
|
+
// until the build child exits and afterBuild() runs. Calling
|
|
160
|
+
// process.exit() here would kill the process (and the child listener)
|
|
161
|
+
// before the build finishes.
|
|
162
|
+
function afterBuild() {
|
|
163
|
+
const pkgPath = resolve(cwd, 'package.json');
|
|
164
|
+
let name = 'app';
|
|
165
|
+
let version = '0.0.0';
|
|
166
|
+
try {
|
|
167
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
168
|
+
name = String(pkg.name ?? 'app').replace(/^@/, '').replace(/\//g, '-');
|
|
169
|
+
version = String(pkg.version ?? '0.0.0');
|
|
142
170
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
catch {
|
|
172
|
+
console.error('No package.json found — is this a tzin project?');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
// --pack: runnable tarball (dist + manifest + container files when present).
|
|
176
|
+
if (pack) {
|
|
177
|
+
const out = resolve(cwd, `${name}-${version}.tgz`);
|
|
178
|
+
const files = ['dist', 'package.json'];
|
|
179
|
+
for (const f of ['Dockerfile', 'docker-compose.yml', '.dockerignore']) {
|
|
180
|
+
if (existsSync(resolve(cwd, f)))
|
|
181
|
+
files.push(f);
|
|
182
|
+
}
|
|
183
|
+
console.log(`Packing ${files.join(', ')} → ${out}`);
|
|
184
|
+
const tar = spawn('tar', ['-czf', out, ...files], { cwd, stdio: 'inherit' });
|
|
185
|
+
tar.on('exit', (tarCode) => {
|
|
186
|
+
if (tarCode !== 0)
|
|
187
|
+
process.exit(tarCode ?? 1);
|
|
188
|
+
console.log(`\n✓ Packed ${out}`);
|
|
189
|
+
console.log(`Unpack + run: tar -xzf ${name}-${version}.tgz && node dist/index.js`);
|
|
190
|
+
if (dockerTag)
|
|
191
|
+
dockerBuild();
|
|
192
|
+
else
|
|
193
|
+
process.exit(0);
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (dockerTag) {
|
|
198
|
+
dockerBuild();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
console.log('Run: node dist/index.js');
|
|
202
|
+
console.log('Tip: tzin deploy --target node --pack [--docker <tag> [--push]] for a runnable artifact');
|
|
203
|
+
process.exit(0);
|
|
171
204
|
}
|
|
172
|
-
|
|
173
|
-
|
|
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);
|
|
205
|
+
function dockerBuild() {
|
|
206
|
+
if (!dockerTag)
|
|
182
207
|
return;
|
|
208
|
+
if (!existsSync(resolve(cwd, 'Dockerfile'))) {
|
|
209
|
+
console.error('No Dockerfile found. Copy the reference Dockerfile from the tzin repo (see docs/deployment.md).');
|
|
210
|
+
process.exit(1);
|
|
183
211
|
}
|
|
184
|
-
console.log(`
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
212
|
+
console.log(`Building Docker image ${dockerTag}...`);
|
|
213
|
+
const args = ['build', '-t', dockerTag, '.'];
|
|
214
|
+
const child = spawn('docker', args, { cwd, stdio: 'inherit' });
|
|
215
|
+
child.on('exit', (buildCode) => {
|
|
216
|
+
if (buildCode !== 0)
|
|
217
|
+
process.exit(buildCode ?? 1);
|
|
218
|
+
console.log(`\n✓ Image built: ${dockerTag}`);
|
|
219
|
+
if (!push) {
|
|
220
|
+
console.log(`Run: docker run -p 3000:3000 --env PORT=3000 ${dockerTag}`);
|
|
221
|
+
process.exit(0);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
console.log(`Pushing ${dockerTag}...`);
|
|
225
|
+
const pushChild = spawn('docker', ['push', dockerTag], { cwd, stdio: 'inherit' });
|
|
226
|
+
pushChild.on('exit', (pushCode) => process.exit(pushCode ?? 1));
|
|
227
|
+
});
|
|
228
|
+
}
|
|
188
229
|
}
|
|
189
230
|
}
|
|
190
231
|
// ── generate ─────────────────────────────────────────────────────────
|
package/dist/config.d.ts
CHANGED
|
@@ -21,4 +21,10 @@ export interface TzinConfig {
|
|
|
21
21
|
entry?: string;
|
|
22
22
|
}
|
|
23
23
|
export declare function loadConfig(cwd?: string): TzinConfig | null;
|
|
24
|
+
/**
|
|
25
|
+
* Same as {@link loadConfig}, but also reads a TypeScript `tzin.config.ts`
|
|
26
|
+
* through a dynamic import. Needs a TS loader in the process (the dev server
|
|
27
|
+
* runs under `tsx`).
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadConfigAsync(cwd?: string): Promise<TzinConfig | null>;
|
|
24
30
|
export declare function defineConfig(config: TzinConfig): TzinConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,32 +1,58 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
3
5
|
const CONFIG_FILES = [
|
|
4
6
|
'tzin.config.ts',
|
|
5
7
|
'tzin.config.js',
|
|
6
8
|
'tzin.config.mjs',
|
|
7
9
|
'tzin.config.json',
|
|
8
10
|
];
|
|
9
|
-
|
|
11
|
+
function findConfigFile(cwd) {
|
|
10
12
|
for (const file of CONFIG_FILES) {
|
|
11
13
|
const path = resolve(cwd, file);
|
|
12
|
-
if (existsSync(path))
|
|
13
|
-
|
|
14
|
-
// For .ts files, we need to use a loader
|
|
15
|
-
if (file.endsWith('.ts')) {
|
|
16
|
-
// Return null for now - will be handled by tsx loader
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
// For .js/.mjs, import dynamically
|
|
20
|
-
const config = require(path);
|
|
21
|
-
return config.default || config;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
14
|
+
if (existsSync(path))
|
|
15
|
+
return { file, path };
|
|
27
16
|
}
|
|
28
17
|
return null;
|
|
29
18
|
}
|
|
19
|
+
export function loadConfig(cwd = process.cwd()) {
|
|
20
|
+
const found = findConfigFile(cwd);
|
|
21
|
+
if (!found)
|
|
22
|
+
return null;
|
|
23
|
+
// A .ts config needs a TS loader; only loadConfigAsync can do that.
|
|
24
|
+
if (found.file.endsWith('.ts'))
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
// createRequire, not a bare require(): the package is ESM, where a
|
|
28
|
+
// bare require() is undefined and would silently drop the config.
|
|
29
|
+
const config = createRequire(import.meta.url)(found.path);
|
|
30
|
+
return config.default || config;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Same as {@link loadConfig}, but also reads a TypeScript `tzin.config.ts`
|
|
38
|
+
* through a dynamic import. Needs a TS loader in the process (the dev server
|
|
39
|
+
* runs under `tsx`).
|
|
40
|
+
*/
|
|
41
|
+
export async function loadConfigAsync(cwd = process.cwd()) {
|
|
42
|
+
const found = findConfigFile(cwd);
|
|
43
|
+
if (!found)
|
|
44
|
+
return null;
|
|
45
|
+
if (!found.file.endsWith('.ts'))
|
|
46
|
+
return loadConfig(cwd);
|
|
47
|
+
try {
|
|
48
|
+
const mod = await import(pathToFileURL(found.path).href);
|
|
49
|
+
return (mod.default ?? mod);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.error(`tzin: failed to load ${found.file}: ${err.message}`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
30
56
|
export function defineConfig(config) {
|
|
31
57
|
return config;
|
|
32
58
|
}
|
package/dist/dev-server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { listen } from './node.js';
|
|
5
|
-
import {
|
|
5
|
+
import { loadConfigAsync } from './config.js';
|
|
6
6
|
function pad(s, n) {
|
|
7
7
|
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
8
8
|
}
|
|
@@ -31,7 +31,7 @@ else {
|
|
|
31
31
|
port = nonFlags[1] ? Number(nonFlags[1]) || 3000 : 3000;
|
|
32
32
|
}
|
|
33
33
|
// Load config
|
|
34
|
-
const config =
|
|
34
|
+
const config = (await loadConfigAsync()) ?? {};
|
|
35
35
|
// Auto-detect entry: CLI arg > config > src/app.ts
|
|
36
36
|
const entryFile = entry || config.entry || 'src/app.ts';
|
|
37
37
|
const entryPath = resolve(entryFile);
|
package/dist/index.d.ts
CHANGED
|
@@ -20,5 +20,5 @@ export { wsChannels, type WsRoute, type WsSend, type WsChannelOptions } from './
|
|
|
20
20
|
export { attachChannels } from './ws-node.js';
|
|
21
21
|
export { LocalBus, clusterHubs, type MessageBus } from './bus.js';
|
|
22
22
|
export { cors, type CorsOptions } from './cors.js';
|
|
23
|
-
export { defineConfig, loadConfig, type TzinConfig } from './config.js';
|
|
23
|
+
export { defineConfig, loadConfig, loadConfigAsync, type TzinConfig } from './config.js';
|
|
24
24
|
export { loadRoutes, type RouteLoaderOptions } from './route-loader.js';
|
package/dist/index.js
CHANGED
|
@@ -20,5 +20,5 @@ export { wsChannels } from './ws.js';
|
|
|
20
20
|
export { attachChannels } from './ws-node.js';
|
|
21
21
|
export { LocalBus, clusterHubs } from './bus.js';
|
|
22
22
|
export { cors } from './cors.js';
|
|
23
|
-
export { defineConfig, loadConfig } from './config.js';
|
|
23
|
+
export { defineConfig, loadConfig, loadConfigAsync } from './config.js';
|
|
24
24
|
export { loadRoutes } from './route-loader.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carlos-tzin/tzin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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",
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"type": "module",
|
|
27
27
|
"main": "./dist/index.js",
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
|
+
"bin": {
|
|
30
|
+
"tzin": "./dist/cli.js"
|
|
31
|
+
},
|
|
29
32
|
"exports": {
|
|
30
33
|
".": {
|
|
31
34
|
"types": "./dist/index.d.ts",
|