tw-framework 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/dist/adapters/node/server.mjs +12 -0
- package/dist/hydration-runtime.js +20 -2
- package/dist/node-adapter.ts +81 -3
- package/dist/testing.cjs +47584 -307
- package/dist/tw-runtime-client.mjs +176 -6
- package/dist/tw.mjs +11742 -10876
- package/package.json +1 -1
|
@@ -124,6 +124,7 @@ const server = createServer((req, res) => {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
const file = resolveFile(pathname);
|
|
127
|
+
const spaFallback = file === null;
|
|
127
128
|
const finalPath = file ?? ((existsSync(join(ROOT, "index.html")) && pathname !== "/") ? join(ROOT, "index.html") : null);
|
|
128
129
|
if (!finalPath) {
|
|
129
130
|
// Custom 404 page if the build produced one.
|
|
@@ -136,6 +137,17 @@ const server = createServer((req, res) => {
|
|
|
136
137
|
res.writeHead(404, { "content-type": "text/plain", ...SECURITY_HEADERS });
|
|
137
138
|
return res.end("Not Found");
|
|
138
139
|
}
|
|
140
|
+
// Unknown routes served through the SPA fallback must answer 404 -- a
|
|
141
|
+
// 200 soft-404 hides broken links from crawlers and uptime checks. The
|
|
142
|
+
// built 404.html takes priority; otherwise the shell is served WITH the
|
|
143
|
+
// 404 status so client-side routing still works.
|
|
144
|
+
if (spaFallback) {
|
|
145
|
+
const notFoundPage = join(ROOT, "404.html");
|
|
146
|
+
const spaPath = existsSync(notFoundPage) ? notFoundPage : finalPath;
|
|
147
|
+
res.writeHead(404, { "content-type": "text/html; charset=utf-8", ...SECURITY_HEADERS });
|
|
148
|
+
if (req.method === "HEAD") return res.end();
|
|
149
|
+
return createReadStream(spaPath).pipe(res);
|
|
150
|
+
}
|
|
139
151
|
|
|
140
152
|
const stat = statSync(finalPath);
|
|
141
153
|
const ext = extname(finalPath).toLowerCase();
|
|
@@ -269,7 +269,24 @@
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
function navigate(path, push) {
|
|
272
|
-
|
|
272
|
+
// loading.tw skeleton (docs/commands-reference.md): the boundary ships
|
|
273
|
+
// hidden; show it while the navigation fetch runs, hide it again after.
|
|
274
|
+
var lb = document.querySelector("[data-loading-boundary]");
|
|
275
|
+
function hideSkeleton() {
|
|
276
|
+
if (lb) try { lb.style.display = "none"; } catch (e) { /* gone */ }
|
|
277
|
+
}
|
|
278
|
+
if (lb) {
|
|
279
|
+
// Show via the framework's own display style (empty string would
|
|
280
|
+
// fall back to the default block and fight the inline display:none).
|
|
281
|
+
try { lb.style.display = "block"; } catch (e) { /* ignore */ }
|
|
282
|
+
}
|
|
283
|
+
fetch(path, {
|
|
284
|
+
headers: { "X-TW-Navigate": "1" },
|
|
285
|
+
// Hover-prefetch warms the HTTP cache WITHOUT the navigate header;
|
|
286
|
+
// a real navigation must bypass that cache so the server can render
|
|
287
|
+
// `(.)page.tw` interceptors (and other nav-specific output).
|
|
288
|
+
cache: "reload",
|
|
289
|
+
})
|
|
273
290
|
.then(function (r) { return r.text(); })
|
|
274
291
|
.then(function (html) {
|
|
275
292
|
swapPage(html, path);
|
|
@@ -277,8 +294,9 @@
|
|
|
277
294
|
try { history.pushState({ tw: 1 }, "", path); } catch (e) { /* ignore */ }
|
|
278
295
|
}
|
|
279
296
|
window.scrollTo(0, 0);
|
|
297
|
+
hideSkeleton();
|
|
280
298
|
})
|
|
281
|
-
.catch(function () { window.location.href = path; });
|
|
299
|
+
.catch(function () { hideSkeleton(); window.location.href = path; });
|
|
282
300
|
}
|
|
283
301
|
|
|
284
302
|
document.addEventListener("click", function (e) {
|
package/dist/node-adapter.ts
CHANGED
|
@@ -72,6 +72,48 @@ if (typeof globalThis.Bun === "undefined") {
|
|
|
72
72
|
this.params = {};
|
|
73
73
|
}
|
|
74
74
|
async json() { return JSON.parse(this._body ? this._body.toString("utf-8") : "{}"); }
|
|
75
|
+
/**
|
|
76
|
+
* multipart/form-data (docs/multipart-forms.md): parse the buffered body
|
|
77
|
+
* into a FormData-like object. Route handlers expect request.formData()
|
|
78
|
+
* to exist exactly like the fetch standard on Bun.
|
|
79
|
+
*/
|
|
80
|
+
async formData(): Promise<any> {
|
|
81
|
+
const bodyStr = this._body ? this._body.toString("binary") : "";
|
|
82
|
+
const ct = String(this.headers?.get?.("content-type") ?? "");
|
|
83
|
+
const bm = /boundary=(?:"([^"]+)"|([^;]+))/i.exec(ct);
|
|
84
|
+
if (!bm) throw new Error("FormData: no multipart boundary");
|
|
85
|
+
const boundary = "--" + (bm[1] ?? bm[2]).trim();
|
|
86
|
+
const out: [string, any][] = [];
|
|
87
|
+
const parts = bodyStr.split(boundary);
|
|
88
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
89
|
+
const raw = parts[i];
|
|
90
|
+
const sep = raw.indexOf("\r\n\r\n");
|
|
91
|
+
if (sep < 0) continue;
|
|
92
|
+
const head = raw.slice(0, sep);
|
|
93
|
+
let data = raw.slice(sep + 4);
|
|
94
|
+
if (data.endsWith("\r\n")) data = data.slice(0, -2);
|
|
95
|
+
const nameM = /name="([^"]*)"/.exec(head);
|
|
96
|
+
if (!nameM) continue;
|
|
97
|
+
const fileM = /filename="([^"]*)"/.exec(head);
|
|
98
|
+
const typeM = /content-type:\s*([^\r\n]+)/i.exec(head);
|
|
99
|
+
if (fileM) {
|
|
100
|
+
const buf = Buffer.from(data, "binary");
|
|
101
|
+
out.push([nameM[1], {
|
|
102
|
+
name: fileM[1],
|
|
103
|
+
type: typeM ? typeM[1].trim() : "application/octet-stream",
|
|
104
|
+
size: buf.length,
|
|
105
|
+
arrayBuffer: async () => buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength),
|
|
106
|
+
}]);
|
|
107
|
+
} else {
|
|
108
|
+
out.push([nameM[1], data]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
entries: () => out[Symbol.iterator](),
|
|
113
|
+
forEach: (fn: any) => out.forEach(([k, v]) => fn(v, k)),
|
|
114
|
+
get: (k: string) => (out.find(([kk]) => kk === k) || [, undefined])[1],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
75
117
|
async text() { return this._body ? this._body.toString("utf-8") : ""; }
|
|
76
118
|
async arrayBuffer() {
|
|
77
119
|
if (!this._body) return new ArrayBuffer(0);
|
|
@@ -98,6 +140,22 @@ if (typeof globalThis.Bun === "undefined") {
|
|
|
98
140
|
const b: any = this._body;
|
|
99
141
|
// Blob (Bun.file) and other web bodies
|
|
100
142
|
if (b && typeof b.arrayBuffer === "function") return await b.arrayBuffer();
|
|
143
|
+
// Web ReadableStream (render stream responses): drain into a buffer.
|
|
144
|
+
if (b && typeof b.getReader === "function") {
|
|
145
|
+
const reader = b.getReader();
|
|
146
|
+
const chunks: Uint8Array[] = [];
|
|
147
|
+
let total = 0;
|
|
148
|
+
for (;;) {
|
|
149
|
+
const { done, value } = await reader.read();
|
|
150
|
+
if (done) break;
|
|
151
|
+
chunks.push(value);
|
|
152
|
+
total += value.byteLength;
|
|
153
|
+
}
|
|
154
|
+
const out = new Uint8Array(total);
|
|
155
|
+
let off = 0;
|
|
156
|
+
for (const c of chunks) { out.set(c, off); off += c.byteLength; }
|
|
157
|
+
return out.buffer;
|
|
158
|
+
}
|
|
101
159
|
const buf = Buffer.isBuffer(b) ? b : Buffer.from(b);
|
|
102
160
|
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
103
161
|
}
|
|
@@ -137,11 +195,31 @@ if (typeof globalThis.Bun === "undefined") {
|
|
|
137
195
|
body: req.method === "GET" || req.method === "HEAD" ? undefined : body,
|
|
138
196
|
});
|
|
139
197
|
const response = await opts.fetch(request);
|
|
140
|
-
const buf = Buffer.from(await response.arrayBuffer());
|
|
141
198
|
const outHeaders = {};
|
|
142
199
|
for (const [k, v] of response.headers.entries()) outHeaders[k] = v;
|
|
143
|
-
|
|
144
|
-
|
|
200
|
+
// Stream ReadableStream bodies chunk-by-chunk. Buffering via
|
|
201
|
+
// arrayBuffer() would HANG forever for persistent streams
|
|
202
|
+
// (signal streaming SSE /_tw/stream never closes).
|
|
203
|
+
const respBody: any = (response as any).body;
|
|
204
|
+
if (respBody && typeof respBody.getReader === "function") {
|
|
205
|
+
res.writeHead(response.status, outHeaders);
|
|
206
|
+
const reader = respBody.getReader();
|
|
207
|
+
const pump = (): void => {
|
|
208
|
+
reader
|
|
209
|
+
.read()
|
|
210
|
+
.then(({ done, value }: { done: boolean; value?: Uint8Array }) => {
|
|
211
|
+
if (done) { try { res.end(); } catch { /* closed */ } return; }
|
|
212
|
+
try { res.write(Buffer.from(value ?? [])); } catch { /* client gone */ }
|
|
213
|
+
pump();
|
|
214
|
+
})
|
|
215
|
+
.catch(() => { try { res.end(); } catch { /* closed */ } });
|
|
216
|
+
};
|
|
217
|
+
pump();
|
|
218
|
+
} else {
|
|
219
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
220
|
+
res.writeHead(response.status, outHeaders);
|
|
221
|
+
res.end(buf);
|
|
222
|
+
}
|
|
145
223
|
} catch (err) {
|
|
146
224
|
try { res.writeHead(500, { "Content-Type": "text/plain" }); res.end("Internal Server Error"); } catch { /* closed */ }
|
|
147
225
|
console.error("[tw:node] request error:", err && err.message);
|