tw-framework 1.0.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 +67 -0
- package/dist/adapters/aws/README.md +37 -0
- package/dist/adapters/aws/deploy-s3.sh +39 -0
- package/dist/adapters/bun/server.ts +30 -0
- package/dist/adapters/caddy/Caddyfile +25 -0
- package/dist/adapters/cloudflare/_headers +12 -0
- package/dist/adapters/cloudflare/_redirects +4 -0
- package/dist/adapters/cloudflare/wrangler.toml +13 -0
- package/dist/adapters/digitalocean/.do/app.yaml +15 -0
- package/dist/adapters/docker/.dockerignore +7 -0
- package/dist/adapters/docker/Dockerfile +35 -0
- package/dist/adapters/docker/Dockerfile.static +27 -0
- package/dist/adapters/docker/docker-compose.yml +8 -0
- package/dist/adapters/firebase/firebase.json +31 -0
- package/dist/adapters/fly/fly.toml +32 -0
- package/dist/adapters/github-pages/.github/workflows/deploy.yml +41 -0
- package/dist/adapters/github-pages/.nojekyll +1 -0
- package/dist/adapters/index.ts +228 -0
- package/dist/adapters/netlify/netlify.toml +23 -0
- package/dist/adapters/nginx/nginx.conf +43 -0
- package/dist/adapters/node/server.mjs +214 -0
- package/dist/adapters/railway/railway.toml +18 -0
- package/dist/adapters/render/render.yaml +27 -0
- package/dist/adapters/vercel/vercel.json +6 -0
- package/dist/node-adapter.ts +187 -0
- package/dist/testing.cjs +1167 -0
- package/dist/tw-runtime-client.mjs +13833 -0
- package/dist/tw.mjs +54515 -0
- package/package.json +50 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TW Framework — Node runtime adapter.
|
|
3
|
+
*
|
|
4
|
+
* TW runs natively on Bun. This adapter lets the SAME codebase run on Node.js
|
|
5
|
+
* (18+) by shimming the few Bun APIs the CLI/server use, plus lightweight
|
|
6
|
+
* Web-standard Request/Response classes (avoids undici's WASM per-request
|
|
7
|
+
* overhead entirely):
|
|
8
|
+
*
|
|
9
|
+
* Bun.serve -> node:http server with fetch-style handler
|
|
10
|
+
* Bun.file -> fs-backed Blob (works with `new Response(file)`)
|
|
11
|
+
* Bun.gzipSync -> zlib.gzipSync
|
|
12
|
+
* Bun.spawn -> child_process.spawnSync
|
|
13
|
+
* Request/Response -> minimal in-house implementations
|
|
14
|
+
*
|
|
15
|
+
* On Bun this file is a no-op. On Node it installs the shims and flags
|
|
16
|
+
* `globalThis.__TW_NODE = true` so commands can pick runtime-specific paths.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import http from "node:http";
|
|
20
|
+
import fsSync from "node:fs";
|
|
21
|
+
import zlib from "node:zlib";
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
|
|
24
|
+
if (typeof globalThis.Bun === "undefined") {
|
|
25
|
+
globalThis.__TW_NODE = true;
|
|
26
|
+
|
|
27
|
+
// --- Minimal Web-standard headers/request/response ---------------------
|
|
28
|
+
|
|
29
|
+
class TwHeaders {
|
|
30
|
+
m: Map<any, any> = new Map();
|
|
31
|
+
constructor(init) {
|
|
32
|
+
this.m = new Map();
|
|
33
|
+
if (!init) return;
|
|
34
|
+
if (init instanceof TwHeaders) { init.m.forEach((v, k) => this.m.set(k, v)); return; }
|
|
35
|
+
if (Array.isArray(init)) { for (const [k, v] of init) this.m.set(String(k).toLowerCase(), String(v)); return; }
|
|
36
|
+
if (typeof init.forEach === "function") { init.forEach((v, k) => this.m.set(String(k).toLowerCase(), String(v))); return; }
|
|
37
|
+
for (const [k, v] of Object.entries(init)) this.m.set(String(k).toLowerCase(), String(v));
|
|
38
|
+
}
|
|
39
|
+
get(k) { const v = this.m.get(String(k).toLowerCase()); return v == null ? null : (Array.isArray(v) ? v.join(", ") : v); }
|
|
40
|
+
set(k, v) { this.m.set(String(k).toLowerCase(), String(v)); }
|
|
41
|
+
has(k) { return this.m.has(String(k).toLowerCase()); }
|
|
42
|
+
delete(k) { this.m.delete(String(k).toLowerCase()); }
|
|
43
|
+
append(k, v) {
|
|
44
|
+
k = String(k).toLowerCase();
|
|
45
|
+
v = String(v);
|
|
46
|
+
if (!this.m.has(k)) { this.m.set(k, v); return; }
|
|
47
|
+
if (k === "set-cookie") {
|
|
48
|
+
// Set-Cookie must stay one header per cookie -- comma-joining breaks
|
|
49
|
+
// attributes like Expires. Everything else joins per RFC 9110.
|
|
50
|
+
const prev = this.m.get(k);
|
|
51
|
+
this.m.set(k, Array.isArray(prev) ? [...prev, v] : [prev, v]);
|
|
52
|
+
} else {
|
|
53
|
+
this.m.set(k, this.m.get(k) + ", " + v);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
entries() { return this.m.entries(); }
|
|
57
|
+
keys() { return this.m.keys(); }
|
|
58
|
+
values() { return this.m.values(); }
|
|
59
|
+
forEach(fn) { this.m.forEach((v, k) => fn(v, k)); }
|
|
60
|
+
[Symbol.iterator]() { return this.m.entries(); }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class TwRequest {
|
|
64
|
+
url: any; method: any; headers: any; _body: any; body: any; query: any; params: any = {};
|
|
65
|
+
constructor(url, init) {
|
|
66
|
+
this.url = url;
|
|
67
|
+
this.method = init?.method || "GET";
|
|
68
|
+
this.headers = new TwHeaders(init?.headers);
|
|
69
|
+
this._body = init?.body;
|
|
70
|
+
this.body = this._body;
|
|
71
|
+
try { this.query = new URL(url).searchParams; } catch { this.query = new URLSearchParams(); }
|
|
72
|
+
this.params = {};
|
|
73
|
+
}
|
|
74
|
+
async json() { return JSON.parse(this._body ? this._body.toString("utf-8") : "{}"); }
|
|
75
|
+
async text() { return this._body ? this._body.toString("utf-8") : ""; }
|
|
76
|
+
async arrayBuffer() {
|
|
77
|
+
if (!this._body) return new ArrayBuffer(0);
|
|
78
|
+
const b = Buffer.isBuffer(this._body) ? this._body : Buffer.from(this._body);
|
|
79
|
+
return b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class TwResponse {
|
|
84
|
+
status: any; headers: any; body: any; _body: any; ok: any; statusText: any = ""; url: any = "";
|
|
85
|
+
constructor(body, init) {
|
|
86
|
+
this.status = init?.status ?? 200;
|
|
87
|
+
this.ok = this.status >= 200 && this.status < 300;
|
|
88
|
+
this.headers = new TwHeaders(init?.headers);
|
|
89
|
+
// Keep `.body` in sync: wrappers rebuild responses as
|
|
90
|
+
// `new Response(res.body, {...})` (security headers, config headers).
|
|
91
|
+
// If `.body` were undefined, the rebuilt response would ship headers
|
|
92
|
+
// without a body and Node would wait on Content-Length forever.
|
|
93
|
+
this.body = body;
|
|
94
|
+
this._body = body;
|
|
95
|
+
}
|
|
96
|
+
async arrayBuffer() {
|
|
97
|
+
if (!this._body) return new ArrayBuffer(0);
|
|
98
|
+
const b: any = this._body;
|
|
99
|
+
// Blob (Bun.file) and other web bodies
|
|
100
|
+
if (b && typeof b.arrayBuffer === "function") return await b.arrayBuffer();
|
|
101
|
+
const buf = Buffer.isBuffer(b) ? b : Buffer.from(b);
|
|
102
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
103
|
+
}
|
|
104
|
+
async text() {
|
|
105
|
+
if (!this._body) return "";
|
|
106
|
+
const ab = await this.arrayBuffer();
|
|
107
|
+
return Buffer.from(ab).toString("utf-8");
|
|
108
|
+
}
|
|
109
|
+
async json() { return JSON.parse(await this.text() || "null"); }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
globalThis.Request = TwRequest as any;
|
|
113
|
+
globalThis.Response = TwResponse as any;
|
|
114
|
+
|
|
115
|
+
// --- Bun API shims --------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
globalThis.Bun = {
|
|
118
|
+
__isNodeShim: true,
|
|
119
|
+
|
|
120
|
+
serve(opts) {
|
|
121
|
+
const port = opts?.port ?? 3000;
|
|
122
|
+
const hostname = opts?.hostname ?? "0.0.0.0";
|
|
123
|
+
const server = http.createServer(async (req, res) => {
|
|
124
|
+
try {
|
|
125
|
+
const url = `http://${req.headers.host || `localhost:${port}`}${req.url || "/"}`;
|
|
126
|
+
const headers = new (TwHeaders as any)();
|
|
127
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
128
|
+
if (v === undefined) continue;
|
|
129
|
+
headers.set(k, Array.isArray(v) ? v.join(", ") : String(v));
|
|
130
|
+
}
|
|
131
|
+
const chunks = [];
|
|
132
|
+
for await (const c of req) chunks.push(c);
|
|
133
|
+
const body = chunks.length ? Buffer.concat(chunks) : undefined;
|
|
134
|
+
const request = new TwRequest(url, {
|
|
135
|
+
method: req.method,
|
|
136
|
+
headers,
|
|
137
|
+
body: req.method === "GET" || req.method === "HEAD" ? undefined : body,
|
|
138
|
+
});
|
|
139
|
+
const response = await opts.fetch(request);
|
|
140
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
141
|
+
const outHeaders = {};
|
|
142
|
+
for (const [k, v] of response.headers.entries()) outHeaders[k] = v;
|
|
143
|
+
res.writeHead(response.status, outHeaders);
|
|
144
|
+
res.end(buf);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
try { res.writeHead(500, { "Content-Type": "text/plain" }); res.end("Internal Server Error"); } catch { /* closed */ }
|
|
147
|
+
console.error("[tw:node] request error:", err && err.message);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
server.listen(port, hostname);
|
|
151
|
+
return {
|
|
152
|
+
port,
|
|
153
|
+
hostname,
|
|
154
|
+
stop() { server.close(); },
|
|
155
|
+
fetch: opts?.fetch,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
file(path) {
|
|
160
|
+
let data = null;
|
|
161
|
+
try { data = fsSync.readFileSync(path); } catch { data = null; }
|
|
162
|
+
const blob = (data !== null ? new Blob([data]) : new Blob([])) as any;
|
|
163
|
+
blob.exists = async () => data !== null;
|
|
164
|
+
blob.json = async () => JSON.parse(data.toString("utf-8"));
|
|
165
|
+
return blob;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
gzipSync(bytes) {
|
|
169
|
+
return zlib.gzipSync(Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes));
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
spawn(cmd, opts) {
|
|
173
|
+
const r = spawnSync(cmd[0], cmd.slice(1), {
|
|
174
|
+
cwd: opts?.cwd,
|
|
175
|
+
stdio: opts?.stdout === "inherit" ? "inherit" : "pipe",
|
|
176
|
+
env: process.env,
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
stdout: r.stdout ?? "",
|
|
180
|
+
stderr: r.stderr ?? "",
|
|
181
|
+
exitCode: r.status ?? 1,
|
|
182
|
+
exited: Promise.resolve(r.status ?? 1),
|
|
183
|
+
success: r.status === 0,
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
} as any;
|
|
187
|
+
}
|