tw-framework 1.0.7 → 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/dist/hydration-runtime.js +19 -3
- package/dist/testing.cjs +1058 -284
- package/dist/tw-server.mjs +56209 -0
- package/dist/tw.mjs +1391 -361
- package/package.json +7 -2
package/dist/testing.cjs
CHANGED
|
@@ -78,11 +78,158 @@ function setSignal(name, value, opts) {
|
|
|
78
78
|
}
|
|
79
79
|
return getSignalHub().setSignal(name, value, session ? { session } : void 0);
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
function sseFrameWithId(payload) {
|
|
82
|
+
try {
|
|
83
|
+
const seq = JSON.parse(payload).seq;
|
|
84
|
+
if (typeof seq === "number") return "id: " + seq + "\ndata: " + payload + "\n\n";
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
return "data: " + payload + "\n\n";
|
|
88
|
+
}
|
|
89
|
+
function createSignalStream(request, url, opts) {
|
|
90
|
+
const hub2 = getSignalHub();
|
|
91
|
+
if (opts && !hub2.__cfg) {
|
|
92
|
+
hub2.__cfg = true;
|
|
93
|
+
try {
|
|
94
|
+
hub2.configure({ maxClients: opts.maxClients, maxHistory: opts.maxHistory });
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (url.searchParams.get("stats")) {
|
|
99
|
+
return new Response(JSON.stringify(hub2.getStats(), null, 2), {
|
|
100
|
+
status: 200,
|
|
101
|
+
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (!hub2.canAcceptClient()) {
|
|
105
|
+
return new Response("Too many signal-stream connections", {
|
|
106
|
+
status: 503,
|
|
107
|
+
headers: { "Content-Type": "text/plain", "Retry-After": "5" }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const names = (url.searchParams.get("s") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
111
|
+
let since = Number(url.searchParams.get("since") ?? "0") || 0;
|
|
112
|
+
if (!since) {
|
|
113
|
+
const lei = Number(request.headers.get("last-event-id") ?? "0") || 0;
|
|
114
|
+
if (lei) since = lei;
|
|
115
|
+
}
|
|
116
|
+
const declared = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const n of names) declared.set(n, "public");
|
|
118
|
+
let session;
|
|
119
|
+
try {
|
|
120
|
+
const header = request.headers.get("cookie") ?? "";
|
|
121
|
+
const m = /(?:^|;\s*)tw_session=([^;]+)/.exec(header);
|
|
122
|
+
if (m) session = decodeURIComponent(m[1].trim().replace(/^"|"$/g, ""));
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
const enc = new TextEncoder();
|
|
126
|
+
const sse = (payload) => enc.encode(sseFrameWithId(payload));
|
|
127
|
+
let registered = null;
|
|
128
|
+
let keepAlive = null;
|
|
129
|
+
const stream = new ReadableStream({
|
|
130
|
+
start(controller) {
|
|
131
|
+
const client = hub2.register(declared, session);
|
|
132
|
+
registered = client;
|
|
133
|
+
const cleanup = () => {
|
|
134
|
+
if (keepAlive) clearInterval(keepAlive);
|
|
135
|
+
hub2.unregister(client.id);
|
|
136
|
+
try {
|
|
137
|
+
controller.close();
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
client.send = (payload) => {
|
|
142
|
+
try {
|
|
143
|
+
controller.enqueue(sse(payload));
|
|
144
|
+
} catch {
|
|
145
|
+
cleanup();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
client.close = cleanup;
|
|
149
|
+
for (const payload of hub2.connectPayloads(since, declared, session)) {
|
|
150
|
+
try {
|
|
151
|
+
controller.enqueue(sse(payload));
|
|
152
|
+
} catch {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
keepAlive = setInterval(() => {
|
|
157
|
+
try {
|
|
158
|
+
controller.enqueue(enc.encode(": keep-alive\n\n"));
|
|
159
|
+
} catch {
|
|
160
|
+
cleanup();
|
|
161
|
+
}
|
|
162
|
+
}, 25e3);
|
|
163
|
+
},
|
|
164
|
+
// v2: when the browser drops the connection (tab close, network
|
|
165
|
+
// loss) cancel fires -- remove THIS hub entry immediately instead
|
|
166
|
+
// of waiting for the next failed send.
|
|
167
|
+
cancel() {
|
|
168
|
+
if (keepAlive) clearInterval(keepAlive);
|
|
169
|
+
try {
|
|
170
|
+
registered?.close?.();
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return new Response(stream, {
|
|
176
|
+
status: 200,
|
|
177
|
+
headers: {
|
|
178
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
179
|
+
"Cache-Control": "no-store",
|
|
180
|
+
"X-Accel-Buffering": "no",
|
|
181
|
+
Connection: "keep-alive"
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async function writeSignalFromClient(request, opts) {
|
|
186
|
+
if (!opts?.clientWrites) {
|
|
187
|
+
return new Response(JSON.stringify({ error: "client signal writes are disabled" }), {
|
|
188
|
+
status: 403,
|
|
189
|
+
headers: { "Content-Type": "application/json" }
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
let body = null;
|
|
193
|
+
try {
|
|
194
|
+
body = await request.json();
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
const name = body?.name;
|
|
198
|
+
if (typeof name !== "string" || !("value" in (body ?? {}))) {
|
|
199
|
+
return new Response(JSON.stringify({ error: "body must be { name, value }" }), {
|
|
200
|
+
status: 400,
|
|
201
|
+
headers: { "Content-Type": "application/json" }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const hub2 = getSignalHub();
|
|
205
|
+
const kind = (hub2.getStats().signals ?? {})[name];
|
|
206
|
+
if (kind && kind !== "public") {
|
|
207
|
+
return new Response(JSON.stringify({ error: "only public signals are client-writable" }), {
|
|
208
|
+
status: 403,
|
|
209
|
+
headers: { "Content-Type": "application/json" }
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const res = hub2.setSignal(name, body.value);
|
|
213
|
+
if (!res.queued) {
|
|
214
|
+
return new Response(JSON.stringify({ error: res.error ?? "rejected" }), {
|
|
215
|
+
status: 400,
|
|
216
|
+
headers: { "Content-Type": "application/json" }
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return new Response(JSON.stringify({ ok: true, kind: res.kind }), {
|
|
220
|
+
status: 202,
|
|
221
|
+
headers: { "Content-Type": "application/json" }
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
var BATCH_MS, HISTORY_LIMIT, MAX_CLIENTS, MAX_VALUE_BYTES, MAX_NAME_LEN, NAME_RE, SignalHub, hub;
|
|
82
225
|
var init_signal_stream = __esm({
|
|
83
226
|
"../../packages/server/tw/routing/signal-stream.ts"() {
|
|
84
227
|
BATCH_MS = 100;
|
|
85
228
|
HISTORY_LIMIT = 1e3;
|
|
229
|
+
MAX_CLIENTS = 500;
|
|
230
|
+
MAX_VALUE_BYTES = 64 * 1024;
|
|
231
|
+
MAX_NAME_LEN = 64;
|
|
232
|
+
NAME_RE = /^[A-Za-z0-9_$.-]{1,64}$/;
|
|
86
233
|
SignalHub = class {
|
|
87
234
|
clients = /* @__PURE__ */ new Map();
|
|
88
235
|
nextClientId = 1;
|
|
@@ -93,12 +240,82 @@ var init_signal_stream = __esm({
|
|
|
93
240
|
pending = /* @__PURE__ */ new Map();
|
|
94
241
|
pendingSession;
|
|
95
242
|
timer = null;
|
|
243
|
+
// v2 stats + caps
|
|
244
|
+
maxClients = MAX_CLIENTS;
|
|
245
|
+
createdAt = Date.now();
|
|
246
|
+
totalUpdates = 0;
|
|
247
|
+
batchesFlushed = 0;
|
|
248
|
+
rejectedCount = 0;
|
|
249
|
+
/** v2: configure caps (from tw.config.ts signalStream options). */
|
|
250
|
+
configure(opts) {
|
|
251
|
+
if (opts?.maxClients && opts.maxClients > 0) this.maxClients = opts.maxClients;
|
|
252
|
+
if (opts?.maxHistory && opts.maxHistory > 0) this.historyLimit = opts.maxHistory;
|
|
253
|
+
}
|
|
254
|
+
historyLimit = HISTORY_LIMIT;
|
|
255
|
+
/** v2: observability for /_tw/stream?stats=1 and window.__tw stats. */
|
|
256
|
+
getStats() {
|
|
257
|
+
const signals = {};
|
|
258
|
+
for (const [n, k] of this.kinds) signals[n] = k;
|
|
259
|
+
return {
|
|
260
|
+
clients: this.clients.size,
|
|
261
|
+
clientCap: this.maxClients,
|
|
262
|
+
seq: this.seq,
|
|
263
|
+
signals,
|
|
264
|
+
valueCount: this.values.size,
|
|
265
|
+
history: this.history.length,
|
|
266
|
+
historyCap: this.historyLimit,
|
|
267
|
+
pending: this.pending.size,
|
|
268
|
+
totalUpdates: this.totalUpdates,
|
|
269
|
+
batchesFlushed: this.batchesFlushed,
|
|
270
|
+
rejected: this.rejectedCount,
|
|
271
|
+
uptimeMs: Date.now() - this.createdAt
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** v2: graceful shutdown -- close every connected stream now. */
|
|
275
|
+
closeAll() {
|
|
276
|
+
for (const c of [...this.clients.values()]) {
|
|
277
|
+
try {
|
|
278
|
+
c.close();
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
this.clients.clear();
|
|
283
|
+
if (this.timer != null) {
|
|
284
|
+
clearTimeout(this.timer);
|
|
285
|
+
this.timer = null;
|
|
286
|
+
}
|
|
287
|
+
this.pending.clear();
|
|
288
|
+
}
|
|
289
|
+
/** v2: connection cap check BEFORE registering a stream client. */
|
|
290
|
+
canAcceptClient() {
|
|
291
|
+
return this.clients.size < this.maxClients;
|
|
292
|
+
}
|
|
293
|
+
/** v2: validate a setSignal name/value without throwing. */
|
|
294
|
+
validateSignal(name, value) {
|
|
295
|
+
if (typeof name !== "string" || name.length === 0 || name.length > MAX_NAME_LEN) {
|
|
296
|
+
return "signal name must be 1-" + MAX_NAME_LEN + " chars";
|
|
297
|
+
}
|
|
298
|
+
if (!NAME_RE.test(name)) return "signal name has invalid characters";
|
|
299
|
+
if (value === void 0) return "signal value is undefined";
|
|
300
|
+
try {
|
|
301
|
+
const size = typeof value === "string" ? value.length : JSON.stringify(value)?.length ?? 0;
|
|
302
|
+
if (size > MAX_VALUE_BYTES) return "signal value exceeds " + MAX_VALUE_BYTES + " bytes";
|
|
303
|
+
} catch {
|
|
304
|
+
return "signal value is not serializable";
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
96
308
|
/**
|
|
97
309
|
* Server-side mutation (route handlers): queue an update, batch-flush.
|
|
98
310
|
* With `session` (derived from the request's tw_session cookie) private
|
|
99
311
|
* signals are delivered only to clients authenticated as that session.
|
|
100
312
|
*/
|
|
101
313
|
setSignal(name, value, opts) {
|
|
314
|
+
const invalid = this.validateSignal(name, value);
|
|
315
|
+
if (invalid) {
|
|
316
|
+
this.rejectedCount++;
|
|
317
|
+
return { queued: false, kind: this.kinds.get(name), error: invalid };
|
|
318
|
+
}
|
|
102
319
|
const kind = this.kinds.get(name) ?? "public";
|
|
103
320
|
this.values.set(name, value);
|
|
104
321
|
this.kinds.set(name, kind);
|
|
@@ -129,9 +346,27 @@ var init_signal_stream = __esm({
|
|
|
129
346
|
this.clients.set(id, client);
|
|
130
347
|
return client;
|
|
131
348
|
}
|
|
349
|
+
/** v2: remove a stream client (its connection died / was cancelled).
|
|
350
|
+
* The old handler overwrote client.close() with a closure that only
|
|
351
|
+
* closed the controller -- the Map entry leaked forever, so every
|
|
352
|
+
* disconnected browser still occupied a slot (and got flushed frames).
|
|
353
|
+
*/
|
|
354
|
+
unregister(id) {
|
|
355
|
+
this.clients.delete(id);
|
|
356
|
+
}
|
|
132
357
|
/** Build the replay/snapshot preamble for a connecting client. */
|
|
133
358
|
connectPayloads(since, declared, session) {
|
|
134
359
|
const out = [];
|
|
360
|
+
if (since <= 0 && this.values.size > 0) {
|
|
361
|
+
const snap = {};
|
|
362
|
+
for (const [name, value] of this.values) {
|
|
363
|
+
if (this.sessionMatches(session, { names: [name], session: this.valueSessions.get(name) }) && this.deliversTo(declared, [name])) snap[name] = value;
|
|
364
|
+
}
|
|
365
|
+
if (Object.keys(snap).length > 0) {
|
|
366
|
+
out.push(this.frame({ v: 1, seq: this.seq, snapshot: snap }));
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
135
370
|
const hasCoverage = since <= this.seq && this.history.length > 0 && since >= this.history[0].seq - 1;
|
|
136
371
|
if (hasCoverage) {
|
|
137
372
|
for (const entry of this.history) {
|
|
@@ -174,7 +409,9 @@ var init_signal_stream = __esm({
|
|
|
174
409
|
for (const [n] of updates) this.valueSessions.set(n, session);
|
|
175
410
|
const payload = this.frame({ v: 1, seq: this.seq, updates });
|
|
176
411
|
this.history.push({ seq: this.seq, payload, names, session });
|
|
177
|
-
|
|
412
|
+
this.totalUpdates += updates.length;
|
|
413
|
+
this.batchesFlushed++;
|
|
414
|
+
if (this.history.length > this.historyLimit) this.history.shift();
|
|
178
415
|
for (const client of this.clients.values()) {
|
|
179
416
|
if (this.sessionMatches(client.session, { names, session }) && this.deliversTo(client.declared, names)) client.send(payload);
|
|
180
417
|
}
|
|
@@ -331,6 +568,8 @@ function matchPath(pattern, pathname) {
|
|
|
331
568
|
}
|
|
332
569
|
function ruleBlocked(rule, request, pathname) {
|
|
333
570
|
const headers = request.headers;
|
|
571
|
+
const hasCondition = rule.userAgent || rule.path || rule.auth || rule.rateLimit || rule.origin || rule.methods && rule.methods.length > 0;
|
|
572
|
+
if (!hasCondition) return true;
|
|
334
573
|
if (rule.userAgent) {
|
|
335
574
|
const ua = (headers.get("user-agent") ?? "").toLowerCase();
|
|
336
575
|
if (!ua) {
|
|
@@ -14584,6 +14823,13 @@ function consumeSignalDeclaration(cursor) {
|
|
|
14584
14823
|
function consumeStateValue(cursor) {
|
|
14585
14824
|
const t2 = cursor.peek();
|
|
14586
14825
|
if (!t2) return "";
|
|
14826
|
+
if (t2.value === "-" || t2.value === "+") {
|
|
14827
|
+
const after = cursor.peek(1);
|
|
14828
|
+
if (after && (after.type === "NUMBER" || after.type === "IDENT" || after.type === "KEYWORD")) {
|
|
14829
|
+
cursor.advance();
|
|
14830
|
+
return (t2.value === "-" ? "-" : "") + String(after.value);
|
|
14831
|
+
}
|
|
14832
|
+
}
|
|
14587
14833
|
if (t2.type === "STRING" || t2.type === "NUMBER" || t2.type === "IDENT" || t2.type === "KEYWORD") {
|
|
14588
14834
|
cursor.advance();
|
|
14589
14835
|
if (t2.type === "STRING") return String(t2.value).replace(/\\([{}"])/g, "$1");
|
|
@@ -15601,6 +15847,20 @@ function parseTWSelector(cursor, errors, state, parseBody) {
|
|
|
15601
15847
|
}
|
|
15602
15848
|
cursor.consumeIf("RBRACE");
|
|
15603
15849
|
} else {
|
|
15850
|
+
for (; ; ) {
|
|
15851
|
+
const t2 = cursor.peek();
|
|
15852
|
+
if (!t2 || t2.type === "RBRACE" || t2.type === "EOF") break;
|
|
15853
|
+
if ((t2.type === "IDENT" || t2.type === "KEYWORD") && ATTR_PAIR_NAMES.has(t2.value)) {
|
|
15854
|
+
const after = cursor.peek(1);
|
|
15855
|
+
if (after && (after.type === "STRING" || after.type === "NUMBER")) {
|
|
15856
|
+
cursor.advance();
|
|
15857
|
+
cursor.advance();
|
|
15858
|
+
el.attrs.push(createAttribute(t2.value, after.value, t2.pos.line, t2.pos.col));
|
|
15859
|
+
continue;
|
|
15860
|
+
}
|
|
15861
|
+
}
|
|
15862
|
+
break;
|
|
15863
|
+
}
|
|
15604
15864
|
el.children = parseBody(["RBRACE"]);
|
|
15605
15865
|
cursor.consumeIf("RBRACE");
|
|
15606
15866
|
}
|
|
@@ -16219,10 +16479,13 @@ function parseWhileStatement(cursor, errors, parseBody) {
|
|
|
16219
16479
|
const whileToken = cursor.advance();
|
|
16220
16480
|
if (!whileToken) return null;
|
|
16221
16481
|
cursor.skipWhitespace();
|
|
16222
|
-
const condToken = cursor.peek();
|
|
16223
16482
|
let condition = "";
|
|
16224
|
-
|
|
16225
|
-
|
|
16483
|
+
while (!cursor.done) {
|
|
16484
|
+
cursor.skipWhitespace();
|
|
16485
|
+
const t2 = cursor.peek();
|
|
16486
|
+
if (!t2 || t2.type === "EOF") break;
|
|
16487
|
+
if (t2.type === "LBRACE" || t2.value === "{") break;
|
|
16488
|
+
condition += (condition ? " " : "") + t2.value;
|
|
16226
16489
|
cursor.advance();
|
|
16227
16490
|
}
|
|
16228
16491
|
const body = parseBlockBody2(cursor, parseBody);
|
|
@@ -16343,13 +16606,133 @@ function parseBlockBody2(cursor, parseBody) {
|
|
|
16343
16606
|
}
|
|
16344
16607
|
return parseBody(["KEYWORD", "RBRACE"]);
|
|
16345
16608
|
}
|
|
16346
|
-
var CLASS_BOUNDARY_BOOLEAN_ATTRS;
|
|
16609
|
+
var ATTR_PAIR_NAMES, CLASS_BOUNDARY_BOOLEAN_ATTRS;
|
|
16347
16610
|
var init_statements = __esm({
|
|
16348
16611
|
"../../packages/compiler/tw/parser/statements/index.ts"() {
|
|
16349
16612
|
init_nodes();
|
|
16350
16613
|
init_recovery();
|
|
16351
16614
|
init_directives();
|
|
16352
16615
|
init_tw();
|
|
16616
|
+
ATTR_PAIR_NAMES = /* @__PURE__ */ new Set([
|
|
16617
|
+
"href",
|
|
16618
|
+
"src",
|
|
16619
|
+
"type",
|
|
16620
|
+
"id",
|
|
16621
|
+
"name",
|
|
16622
|
+
"value",
|
|
16623
|
+
"placeholder",
|
|
16624
|
+
"action",
|
|
16625
|
+
"method",
|
|
16626
|
+
"title",
|
|
16627
|
+
"alt",
|
|
16628
|
+
"rel",
|
|
16629
|
+
"target",
|
|
16630
|
+
"lang",
|
|
16631
|
+
"charset",
|
|
16632
|
+
"content",
|
|
16633
|
+
"width",
|
|
16634
|
+
"height",
|
|
16635
|
+
"loading",
|
|
16636
|
+
"for",
|
|
16637
|
+
"role",
|
|
16638
|
+
"min",
|
|
16639
|
+
"max",
|
|
16640
|
+
"step",
|
|
16641
|
+
"pattern",
|
|
16642
|
+
"accept",
|
|
16643
|
+
"list",
|
|
16644
|
+
"download",
|
|
16645
|
+
"tabindex",
|
|
16646
|
+
"cols",
|
|
16647
|
+
"rows",
|
|
16648
|
+
"wrap",
|
|
16649
|
+
"datetime",
|
|
16650
|
+
"method",
|
|
16651
|
+
"action",
|
|
16652
|
+
"placeholder",
|
|
16653
|
+
"disabled",
|
|
16654
|
+
"required",
|
|
16655
|
+
"readonly",
|
|
16656
|
+
"checked",
|
|
16657
|
+
"selected",
|
|
16658
|
+
"target",
|
|
16659
|
+
"rel",
|
|
16660
|
+
"download",
|
|
16661
|
+
"autocomplete",
|
|
16662
|
+
"autofocus",
|
|
16663
|
+
"min",
|
|
16664
|
+
"max",
|
|
16665
|
+
"step",
|
|
16666
|
+
"pattern",
|
|
16667
|
+
"multiple",
|
|
16668
|
+
"size",
|
|
16669
|
+
"cols",
|
|
16670
|
+
"rows",
|
|
16671
|
+
"wrap",
|
|
16672
|
+
"for",
|
|
16673
|
+
"hidden",
|
|
16674
|
+
"width",
|
|
16675
|
+
"height",
|
|
16676
|
+
"loading",
|
|
16677
|
+
"loop",
|
|
16678
|
+
"muted",
|
|
16679
|
+
"controls",
|
|
16680
|
+
"poster",
|
|
16681
|
+
"preload",
|
|
16682
|
+
"contenteditable",
|
|
16683
|
+
"draggable",
|
|
16684
|
+
"spellcheck",
|
|
16685
|
+
"translate",
|
|
16686
|
+
"accesskey",
|
|
16687
|
+
"tabindex",
|
|
16688
|
+
"role",
|
|
16689
|
+
"slot",
|
|
16690
|
+
"is",
|
|
16691
|
+
"part",
|
|
16692
|
+
"enterkeyhint",
|
|
16693
|
+
"inert",
|
|
16694
|
+
"inputmode",
|
|
16695
|
+
"sandbox",
|
|
16696
|
+
"allow",
|
|
16697
|
+
"allowfullscreen",
|
|
16698
|
+
"referrerpolicy",
|
|
16699
|
+
"fetchpriority",
|
|
16700
|
+
"blocking",
|
|
16701
|
+
"property",
|
|
16702
|
+
"itemprop",
|
|
16703
|
+
"itemtype",
|
|
16704
|
+
"itemid",
|
|
16705
|
+
"itemref",
|
|
16706
|
+
"itemscope",
|
|
16707
|
+
"charset",
|
|
16708
|
+
"http-equiv",
|
|
16709
|
+
"data",
|
|
16710
|
+
"nonce",
|
|
16711
|
+
"crossorigin",
|
|
16712
|
+
"integrity",
|
|
16713
|
+
"async",
|
|
16714
|
+
"defer",
|
|
16715
|
+
"srcset",
|
|
16716
|
+
"sizes",
|
|
16717
|
+
"media",
|
|
16718
|
+
"ping",
|
|
16719
|
+
"coords",
|
|
16720
|
+
"shape",
|
|
16721
|
+
"usemap",
|
|
16722
|
+
"ismap",
|
|
16723
|
+
"kind",
|
|
16724
|
+
"srclang",
|
|
16725
|
+
"default",
|
|
16726
|
+
"autoplay",
|
|
16727
|
+
"playsinline",
|
|
16728
|
+
"enctype",
|
|
16729
|
+
"novalidate",
|
|
16730
|
+
"formaction",
|
|
16731
|
+
"formmethod",
|
|
16732
|
+
"formtarget",
|
|
16733
|
+
"formenctype",
|
|
16734
|
+
"formnovalidate"
|
|
16735
|
+
]);
|
|
16353
16736
|
CLASS_BOUNDARY_BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
|
|
16354
16737
|
"disabled",
|
|
16355
16738
|
"required",
|
|
@@ -24162,7 +24545,7 @@ var init_types4 = __esm({
|
|
|
24162
24545
|
var TW_VERSION, TW_GENERATOR_META;
|
|
24163
24546
|
var init_version = __esm({
|
|
24164
24547
|
"../../packages/compiler/tw/codegen/version.ts"() {
|
|
24165
|
-
TW_VERSION = "1.0.
|
|
24548
|
+
TW_VERSION = "1.0.8";
|
|
24166
24549
|
TW_GENERATOR_META = `<meta name="generator" content="TW Framework ${TW_VERSION}">`;
|
|
24167
24550
|
}
|
|
24168
24551
|
});
|
|
@@ -24229,6 +24612,16 @@ function collectBuiltinImports(program) {
|
|
|
24229
24612
|
for (const dir of program.directives ?? []) {
|
|
24230
24613
|
if (dir.type !== "ImportDirective") continue;
|
|
24231
24614
|
const imp = dir;
|
|
24615
|
+
if (imp.source === "tw" || imp.source === "@tw/server") {
|
|
24616
|
+
for (const item of imp.items ?? []) {
|
|
24617
|
+
const name = item.split(" as ").pop().trim();
|
|
24618
|
+
const spec = TW_NAMED_BUILTINS[name];
|
|
24619
|
+
if (spec) out.set(name, spec);
|
|
24620
|
+
}
|
|
24621
|
+
if (imp.defaultImport && TW_NAMED_BUILTINS[imp.defaultImport]) {
|
|
24622
|
+
out.set(imp.defaultImport, TW_NAMED_BUILTINS[imp.defaultImport]);
|
|
24623
|
+
}
|
|
24624
|
+
}
|
|
24232
24625
|
if (!BUILTIN_COMPONENT_SPECIFIERS.includes(imp.source)) continue;
|
|
24233
24626
|
if (imp.defaultImport) out.set(imp.defaultImport, imp.source);
|
|
24234
24627
|
for (const item of imp.items ?? []) {
|
|
@@ -24298,7 +24691,7 @@ function normalizePrefetchProp(props) {
|
|
|
24298
24691
|
return p;
|
|
24299
24692
|
}
|
|
24300
24693
|
function generateRouterLinkTag(props, textContent) {
|
|
24301
|
-
const href = (props.href ?? "").trim();
|
|
24694
|
+
const href = (props.href ?? props.to ?? "").trim();
|
|
24302
24695
|
if (!href) return `<!-- @tw/RouterLink: missing href -->`;
|
|
24303
24696
|
const opts = {
|
|
24304
24697
|
href,
|
|
@@ -24325,13 +24718,18 @@ function generateBuiltinTag(specifier, props, textContent) {
|
|
|
24325
24718
|
if (specifier === "@tw/RouterLink") return generateRouterLinkTag(props, textContent);
|
|
24326
24719
|
return null;
|
|
24327
24720
|
}
|
|
24328
|
-
var BUILTIN_COMPONENT_SPECIFIERS, imageConfig;
|
|
24721
|
+
var BUILTIN_COMPONENT_SPECIFIERS, imageConfig, TW_NAMED_BUILTINS;
|
|
24329
24722
|
var init_builtin_components = __esm({
|
|
24330
24723
|
"../../packages/compiler/tw/codegen/builtin-components.ts"() {
|
|
24331
24724
|
init_tw2();
|
|
24332
24725
|
init_tw3();
|
|
24333
24726
|
BUILTIN_COMPONENT_SPECIFIERS = ["@tw/optImage", "@tw/RouterLink"];
|
|
24334
24727
|
imageConfig = DEFAULT_IMAGE_CONFIG;
|
|
24728
|
+
TW_NAMED_BUILTINS = {
|
|
24729
|
+
RouterLink: "@tw/RouterLink",
|
|
24730
|
+
optImage: "@tw/optImage",
|
|
24731
|
+
Image: "@tw/optImage"
|
|
24732
|
+
};
|
|
24335
24733
|
}
|
|
24336
24734
|
});
|
|
24337
24735
|
|
|
@@ -24968,6 +25366,58 @@ function stripTssComments(src) {
|
|
|
24968
25366
|
return out;
|
|
24969
25367
|
}
|
|
24970
25368
|
function compileTSS(source) {
|
|
25369
|
+
{
|
|
25370
|
+
let depth = 0;
|
|
25371
|
+
let line = 1;
|
|
25372
|
+
let inString = null;
|
|
25373
|
+
let inComment = false;
|
|
25374
|
+
let inUrl = false;
|
|
25375
|
+
for (let i = 0; i < source.length; i++) {
|
|
25376
|
+
const ch = source[i];
|
|
25377
|
+
const prev = i > 0 ? source[i - 1] : "";
|
|
25378
|
+
if (ch === "\n") {
|
|
25379
|
+
line++;
|
|
25380
|
+
inComment = false;
|
|
25381
|
+
continue;
|
|
25382
|
+
}
|
|
25383
|
+
if (inComment) continue;
|
|
25384
|
+
if (inString) {
|
|
25385
|
+
if (ch === inString && prev !== "\\") inString = null;
|
|
25386
|
+
continue;
|
|
25387
|
+
}
|
|
25388
|
+
if (inUrl) {
|
|
25389
|
+
if (ch === ")") inUrl = false;
|
|
25390
|
+
continue;
|
|
25391
|
+
}
|
|
25392
|
+
if (ch === "/" && source[i + 1] === "/" && source.slice(Math.max(0, i - 3), i).includes(":")) {
|
|
25393
|
+
inUrl = true;
|
|
25394
|
+
continue;
|
|
25395
|
+
}
|
|
25396
|
+
if (ch === "u" && source.slice(i, i + 4) === "url(") {
|
|
25397
|
+
inUrl = true;
|
|
25398
|
+
i += 3;
|
|
25399
|
+
continue;
|
|
25400
|
+
}
|
|
25401
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
25402
|
+
inComment = true;
|
|
25403
|
+
continue;
|
|
25404
|
+
}
|
|
25405
|
+
if (ch === '"' || ch === "'") {
|
|
25406
|
+
inString = ch;
|
|
25407
|
+
continue;
|
|
25408
|
+
}
|
|
25409
|
+
if (ch === "{") depth++;
|
|
25410
|
+
else if (ch === "}") {
|
|
25411
|
+
depth--;
|
|
25412
|
+
if (depth < 0) {
|
|
25413
|
+
throw new Error("TW303: unbalanced '}' in stylesheet (line " + line + ")\n " + (source.split("\n")[line - 1] ?? "").trim().slice(0, 80));
|
|
25414
|
+
}
|
|
25415
|
+
}
|
|
25416
|
+
}
|
|
25417
|
+
if (depth !== 0) {
|
|
25418
|
+
throw new Error("TW303: " + depth + " unclosed '{' in stylesheet -- a rule block is never closed\n last line: " + (source.split("\n").pop() ?? "").trim().slice(0, 80));
|
|
25419
|
+
}
|
|
25420
|
+
}
|
|
24971
25421
|
const __twOut = __twCompileTSSInner(source);
|
|
24972
25422
|
const leftover = __twOut.match(/\$[A-Za-z_][\w-]*/g);
|
|
24973
25423
|
if (leftover && !/\$\{/.test(__twOut)) {
|
|
@@ -25512,11 +25962,11 @@ function emit(items, parent, env, mixins, out) {
|
|
|
25512
25962
|
}
|
|
25513
25963
|
function compileSCSS(source) {
|
|
25514
25964
|
const cleaned0 = stripComments(source);
|
|
25515
|
-
const UNSUPPORTED = /@(import|
|
|
25965
|
+
const UNSUPPORTED = /@(import|extend|use|forward|function|each|if|else|for|while)\b/g;
|
|
25516
25966
|
const m = cleaned0.match(UNSUPPORTED);
|
|
25517
25967
|
if (m) {
|
|
25518
25968
|
const seen = Array.from(new Set(m.map((x) => x.trim())));
|
|
25519
|
-
console.warn("[scss] unsupported at-rule(s) ignored: " + seen.join(", ") + " -- TW's SCSS subset supports nesting, $vars
|
|
25969
|
+
console.warn("[scss] unsupported at-rule(s) ignored: " + seen.join(", ") + " -- TW's SCSS subset supports nesting, $vars, & references and @mixin/@include");
|
|
25520
25970
|
}
|
|
25521
25971
|
const items = parseItems(stripComments(source));
|
|
25522
25972
|
const out = [];
|
|
@@ -25656,13 +26106,13 @@ function collectTssImports(programs, ctx) {
|
|
|
25656
26106
|
else ctx.inlineStyles.push(css);
|
|
25657
26107
|
}
|
|
25658
26108
|
} catch (e) {
|
|
25659
|
-
if (e && typeof e.message === "string" && /^TW30[
|
|
26109
|
+
if (e && typeof e.message === "string" && /^TW30[123]/.test(e.message)) throw e;
|
|
25660
26110
|
}
|
|
25661
26111
|
}
|
|
25662
26112
|
}
|
|
25663
26113
|
}
|
|
25664
26114
|
} catch (e) {
|
|
25665
|
-
if (e && typeof e.message === "string" && /^TW30[
|
|
26115
|
+
if (e && typeof e.message === "string" && /^TW30[123]/.test(e.message)) throw e;
|
|
25666
26116
|
}
|
|
25667
26117
|
}
|
|
25668
26118
|
function resolveTssPath(fromDir, rel) {
|
|
@@ -25882,6 +26332,10 @@ function generateSuspense(el, ctx) {
|
|
|
25882
26332
|
function generateElement(el, ctx) {
|
|
25883
26333
|
const tag = el.tag;
|
|
25884
26334
|
const voidTag = VOID_TAGS5.has(tag.toLowerCase());
|
|
26335
|
+
if (tag.length > 0 && tag[0] === tag[0].toUpperCase() && !["Link", "Suspense"].includes(tag) && !componentRegistry.has(tag) && !resolveBuiltin(tag, activeBuiltinImports)) {
|
|
26336
|
+
console.warn("[tw] unknown component <" + tag + "> (" + el.line + ":" + el.col + ") -- no import resolves it; nothing was rendered");
|
|
26337
|
+
return "<!-- TW: unknown component '" + tag + "' -- check the import -->";
|
|
26338
|
+
}
|
|
25885
26339
|
{
|
|
25886
26340
|
const builtinSpec = resolveBuiltin(tag, activeBuiltinImports);
|
|
25887
26341
|
if (builtinSpec) {
|
|
@@ -26141,7 +26595,13 @@ function generateComponent(comp, ctx) {
|
|
|
26141
26595
|
ctx.slotContent = savedSlot;
|
|
26142
26596
|
ctx.inHead = compSavedInHead;
|
|
26143
26597
|
} else {
|
|
26144
|
-
|
|
26598
|
+
const known = ["Link", "Suspense"];
|
|
26599
|
+
if (!known.includes(compName)) {
|
|
26600
|
+
console.warn("[tw] unknown component <" + compName + "> (" + comp.line + ":" + comp.col + ") -- unresolvable import; nothing was rendered");
|
|
26601
|
+
childrenHTML = "<!-- TW: unknown component '" + compName + "' -- check the import -->";
|
|
26602
|
+
} else {
|
|
26603
|
+
childrenHTML = comp.children.map((c) => generateNode(c, ctx)).join("");
|
|
26604
|
+
}
|
|
26145
26605
|
}
|
|
26146
26606
|
ctx.stateVars = savedStateVars;
|
|
26147
26607
|
ctx.componentStack.pop();
|
|
@@ -29938,13 +30398,27 @@ ${indentation})` : type;
|
|
|
29938
30398
|
});
|
|
29939
30399
|
|
|
29940
30400
|
// ../../packages/compiler/tw/codegen/vdom.ts
|
|
30401
|
+
function stateLiteralForRender(raw) {
|
|
30402
|
+
const evalNum = (v) => typeof v === "number" || typeof v === "boolean" ? String(v) : null;
|
|
30403
|
+
try {
|
|
30404
|
+
const n = evalNum(JSON.parse(raw));
|
|
30405
|
+
if (n !== null) return n;
|
|
30406
|
+
} catch {
|
|
30407
|
+
}
|
|
30408
|
+
try {
|
|
30409
|
+
const n = evalNum(new Function("return (" + raw + ")")());
|
|
30410
|
+
if (n !== null) return n;
|
|
30411
|
+
} catch {
|
|
30412
|
+
}
|
|
30413
|
+
return raw;
|
|
30414
|
+
}
|
|
29941
30415
|
function generateWithLayout(layoutProgram, pageProgram, stateVars) {
|
|
29942
30416
|
const ctx = createContext("ssr");
|
|
29943
30417
|
if (stateVars) ctx.stateVars = { ...stateVars };
|
|
29944
30418
|
for (const dir of pageProgram.directives || []) {
|
|
29945
30419
|
if (dir.type === "StateDirective") {
|
|
29946
30420
|
for (const decl of dir.declarations || []) {
|
|
29947
|
-
ctx.stateVars[decl.name] = decl.value;
|
|
30421
|
+
ctx.stateVars[decl.name] = stateLiteralForRender(decl.value);
|
|
29948
30422
|
}
|
|
29949
30423
|
}
|
|
29950
30424
|
}
|
|
@@ -29976,7 +30450,7 @@ function generateWithLayoutChain(layoutPrograms, pageProgram, stateVars) {
|
|
|
29976
30450
|
for (const dir of pageProgram.directives || []) {
|
|
29977
30451
|
if (dir.type === "StateDirective") {
|
|
29978
30452
|
for (const decl of dir.declarations || []) {
|
|
29979
|
-
ctx2.stateVars[decl.name] = decl.value;
|
|
30453
|
+
ctx2.stateVars[decl.name] = stateLiteralForRender(decl.value);
|
|
29980
30454
|
}
|
|
29981
30455
|
}
|
|
29982
30456
|
}
|
|
@@ -30167,7 +30641,9 @@ function generate(program, stateVars) {
|
|
|
30167
30641
|
ctx.hasInteractivity = true;
|
|
30168
30642
|
continue;
|
|
30169
30643
|
}
|
|
30170
|
-
|
|
30644
|
+
if (!(decl.name in ctx.stateVars)) {
|
|
30645
|
+
ctx.stateVars[decl.name] = stateLiteralForRender(decl.value);
|
|
30646
|
+
}
|
|
30171
30647
|
let v = decl.value;
|
|
30172
30648
|
try {
|
|
30173
30649
|
v = JSON.parse(decl.value);
|
|
@@ -30500,7 +30976,11 @@ var init_codes = __esm({
|
|
|
30500
30976
|
TW090: { message: "cache { } requires a revalidate window or a life profile", severity: "error", category: "syntax" },
|
|
30501
30977
|
TW091: { message: "fn cached handler is impure (request.cookies/headers/body or setSignal)", severity: "error", category: "semantic" },
|
|
30502
30978
|
TW092: { message: "Unknown cache profile", severity: "error", category: "semantic" },
|
|
30503
|
-
TW093: { message: "Non-deterministic call in a cached handler (value freezes into the cache entry)", severity: "warning", category: "best-practice" }
|
|
30979
|
+
TW093: { message: "Non-deterministic call in a cached handler (value freezes into the cache entry)", severity: "warning", category: "best-practice" },
|
|
30980
|
+
TW094: { message: "Invalid state literal -- array/object elements must be comma-separated (and objects need colons)", severity: "error", category: "syntax" },
|
|
30981
|
+
TW095: { message: "Invalid for-loop iterable -- use {varName} or a valid comma-separated literal", severity: "error", category: "syntax" },
|
|
30982
|
+
TW096: { message: "Unknown state variable in interpolation (renders empty)", severity: "warning", category: "semantic" },
|
|
30983
|
+
TW097: { message: "while with a comparison renders its body ONCE at build time (state cannot mutate during SSR) -- use for loops for repetition", severity: "warning", category: "semantic" }
|
|
30504
30984
|
};
|
|
30505
30985
|
}
|
|
30506
30986
|
});
|
|
@@ -30821,6 +31301,124 @@ function checkUnusedDeclarations(program, filePath, diags) {
|
|
|
30821
31301
|
}
|
|
30822
31302
|
}
|
|
30823
31303
|
}
|
|
31304
|
+
function literalIsBroken(value) {
|
|
31305
|
+
const v = value.trim();
|
|
31306
|
+
if (!v.startsWith("[") && !v.startsWith("{")) return false;
|
|
31307
|
+
try {
|
|
31308
|
+
new Function("return (" + v + ")");
|
|
31309
|
+
return false;
|
|
31310
|
+
} catch {
|
|
31311
|
+
return true;
|
|
31312
|
+
}
|
|
31313
|
+
}
|
|
31314
|
+
function checkStateLiterals(program, filePath, diags) {
|
|
31315
|
+
for (const decl of collectStateDecls(program)) {
|
|
31316
|
+
if (decl.signalKind === "derived") continue;
|
|
31317
|
+
if (literalIsBroken(String(decl.value))) {
|
|
31318
|
+
diags.push(createDiagnostic(
|
|
31319
|
+
"TW094",
|
|
31320
|
+
decl.line,
|
|
31321
|
+
decl.col,
|
|
31322
|
+
filePath,
|
|
31323
|
+
`${decl.name} = ${decl.value}`,
|
|
31324
|
+
[
|
|
31325
|
+
`Comma-separate array elements: ${decl.name} = ["a", "b", "c"]`,
|
|
31326
|
+
`Objects need colons: ${decl.name} = { key: "value" }`
|
|
31327
|
+
]
|
|
31328
|
+
));
|
|
31329
|
+
}
|
|
31330
|
+
}
|
|
31331
|
+
}
|
|
31332
|
+
function checkLoopIterables(program, filePath, diags) {
|
|
31333
|
+
forEachNode(program, (node) => {
|
|
31334
|
+
if (!node || node.type !== "For") return;
|
|
31335
|
+
const iterable = String(node.iterable ?? "").trim();
|
|
31336
|
+
if (!iterable) {
|
|
31337
|
+
diags.push(createDiagnostic(
|
|
31338
|
+
"TW095",
|
|
31339
|
+
node.line,
|
|
31340
|
+
node.col,
|
|
31341
|
+
filePath,
|
|
31342
|
+
"for-loop has no iterable",
|
|
31343
|
+
[`Use: for item in {list} { ... }`]
|
|
31344
|
+
));
|
|
31345
|
+
return;
|
|
31346
|
+
}
|
|
31347
|
+
if (/^\{[A-Za-z_][\w.]*\}$/.test(iterable)) return;
|
|
31348
|
+
if (/^[A-Za-z_][\w.]*$/.test(iterable)) return;
|
|
31349
|
+
if (literalIsBroken(iterable)) {
|
|
31350
|
+
diags.push(createDiagnostic(
|
|
31351
|
+
"TW095",
|
|
31352
|
+
node.line,
|
|
31353
|
+
node.col,
|
|
31354
|
+
filePath,
|
|
31355
|
+
"for x in " + iterable,
|
|
31356
|
+
[
|
|
31357
|
+
`Wrap a state variable in braces: for item in {items}`,
|
|
31358
|
+
`Literals need commas: for item in {["a", "b", "c"]}`
|
|
31359
|
+
]
|
|
31360
|
+
));
|
|
31361
|
+
}
|
|
31362
|
+
});
|
|
31363
|
+
}
|
|
31364
|
+
function checkUnknownStateVars(program, filePath, diags) {
|
|
31365
|
+
const known = /* @__PURE__ */ new Set();
|
|
31366
|
+
for (const decl of collectStateDecls(program)) known.add(decl.name);
|
|
31367
|
+
forEachNode(program, (node) => {
|
|
31368
|
+
if (node && node.type === "For" && node.varName) known.add(node.varName);
|
|
31369
|
+
if (node && node.type === "For" && node.indexName) known.add(node.indexName);
|
|
31370
|
+
});
|
|
31371
|
+
const BUILTINS2 = /* @__PURE__ */ new Set([
|
|
31372
|
+
"params",
|
|
31373
|
+
"props",
|
|
31374
|
+
"Math",
|
|
31375
|
+
"Date",
|
|
31376
|
+
"JSON",
|
|
31377
|
+
"String",
|
|
31378
|
+
"Number",
|
|
31379
|
+
"Boolean",
|
|
31380
|
+
"Array",
|
|
31381
|
+
"Object",
|
|
31382
|
+
"window",
|
|
31383
|
+
"true",
|
|
31384
|
+
"false",
|
|
31385
|
+
"null",
|
|
31386
|
+
"undefined"
|
|
31387
|
+
]);
|
|
31388
|
+
forEachNode(program, (node) => {
|
|
31389
|
+
if (!isText(node)) return;
|
|
31390
|
+
const text = String(node.value ?? "");
|
|
31391
|
+
if (!text.includes("{")) return;
|
|
31392
|
+
for (const m of text.matchAll(/\{\s*([A-Za-z_][\w]*)\s*\}/g)) {
|
|
31393
|
+
const name = m[1];
|
|
31394
|
+
if (known.has(name) || BUILTINS2.has(name)) continue;
|
|
31395
|
+
diags.push(createDiagnostic(
|
|
31396
|
+
"TW096",
|
|
31397
|
+
node.line,
|
|
31398
|
+
node.col,
|
|
31399
|
+
filePath,
|
|
31400
|
+
`{${name}}`,
|
|
31401
|
+
[`Declare it in the state block, or check the spelling`]
|
|
31402
|
+
));
|
|
31403
|
+
}
|
|
31404
|
+
});
|
|
31405
|
+
}
|
|
31406
|
+
function checkWhileComparisons(program, filePath, diags) {
|
|
31407
|
+
forEachNode(program, (node) => {
|
|
31408
|
+
if (!node || node.type !== "While") return;
|
|
31409
|
+
const cond = String(node.condition ?? "");
|
|
31410
|
+
if (/[<>=!]/.test(cond)) {
|
|
31411
|
+
diags.push(createDiagnostic(
|
|
31412
|
+
"TW097",
|
|
31413
|
+
node.line,
|
|
31414
|
+
node.col,
|
|
31415
|
+
filePath,
|
|
31416
|
+
"while " + cond,
|
|
31417
|
+
["Use `for item in {list}` for repetition", "while is for boolean flags (state { loading = true })"]
|
|
31418
|
+
));
|
|
31419
|
+
}
|
|
31420
|
+
});
|
|
31421
|
+
}
|
|
30824
31422
|
var HTML_ELEMENTS2, HTML_ATTRIBUTES2, GLOBAL_ATTRIBUTES2, CSS_PROPERTIES2, CSS_ALIASES2, EVENT_TYPES2, ARIA_ROLES2, ARIA_ATTRIBUTES2;
|
|
30825
31423
|
var init_rules = __esm({
|
|
30826
31424
|
"../../packages/compiler/tw/diagnostics/rules.ts"() {
|
|
@@ -31711,6 +32309,10 @@ function diagnose(program, filePath) {
|
|
|
31711
32309
|
checkSemanticErrors(program, filePath, diagnostics);
|
|
31712
32310
|
checkDirectives(program, filePath, diagnostics);
|
|
31713
32311
|
checkUnusedDeclarations(program, filePath, diagnostics);
|
|
32312
|
+
checkStateLiterals(program, filePath, diagnostics);
|
|
32313
|
+
checkLoopIterables(program, filePath, diagnostics);
|
|
32314
|
+
checkUnknownStateVars(program, filePath, diagnostics);
|
|
32315
|
+
checkWhileComparisons(program, filePath, diagnostics);
|
|
31714
32316
|
return diagnostics;
|
|
31715
32317
|
}
|
|
31716
32318
|
var RuleEngine;
|
|
@@ -41625,6 +42227,7 @@ var init_render_pipeline = __esm({
|
|
|
41625
42227
|
init_twm_loader();
|
|
41626
42228
|
init_scanner();
|
|
41627
42229
|
init_layout_chain();
|
|
42230
|
+
init_signal_stream();
|
|
41628
42231
|
init_helpers();
|
|
41629
42232
|
init_tw6();
|
|
41630
42233
|
init_twm_loader();
|
|
@@ -41778,6 +42381,17 @@ var init_render_pipeline = __esm({
|
|
|
41778
42381
|
try {
|
|
41779
42382
|
const pageState = { ...stateVars ?? {}, ...match.params ?? {}, params: match.params ?? {} };
|
|
41780
42383
|
const requestVarKeys = new Set(Object.keys({ ...stateVars ?? {}, ...match.params ?? {} }));
|
|
42384
|
+
try {
|
|
42385
|
+
const hub2 = getSignalHub();
|
|
42386
|
+
const snap = hub2.snapshot();
|
|
42387
|
+
if (snap && snap.snapshot && typeof snap.snapshot === "object") {
|
|
42388
|
+
for (const [k, v] of Object.entries(snap.snapshot)) {
|
|
42389
|
+
pageState[k] = v;
|
|
42390
|
+
requestVarKeys.add(k);
|
|
42391
|
+
}
|
|
42392
|
+
}
|
|
42393
|
+
} catch {
|
|
42394
|
+
}
|
|
41781
42395
|
const pageCompiled = this.compileFile(ctx.page, pageState);
|
|
41782
42396
|
{
|
|
41783
42397
|
const seed = pageCompiled.stateSeed;
|
|
@@ -41823,24 +42437,30 @@ var init_render_pipeline = __esm({
|
|
|
41823
42437
|
let layoutTitle;
|
|
41824
42438
|
const liftedParts = [];
|
|
41825
42439
|
{
|
|
41826
|
-
|
|
42440
|
+
const collectedTitles = [];
|
|
42441
|
+
for (const headMatch of bodyHTML.matchAll(/<head(?:\s[^>]*)?>([\s\S]*?)<\/head\s*>/gi)) {
|
|
41827
42442
|
const headInner = headMatch[1];
|
|
41828
42443
|
const tMatch = headInner.match(/<title>([^<]*)<\/title>/i);
|
|
41829
|
-
if (tMatch)
|
|
41830
|
-
|
|
42444
|
+
if (tMatch) collectedTitles.push(tMatch[1].trim().replace(/&(amp|lt|gt|quot);/g, (m, k) => k === "amp" ? String.fromCharCode(38) : k === "lt" ? String.fromCharCode(60) : k === "gt" ? String.fromCharCode(62) : String.fromCharCode(34)));
|
|
42445
|
+
else collectedTitles.push("");
|
|
42446
|
+
const kids = headInner.replace(/<title>[^<]*<\/title>/gi, "").replace(/<meta[^>]+charset[^>]*>/gi, "").replace(/<meta[^>]+viewport[^>]*>/gi, "").replace(/<meta[^>]+generator[^>]*>/gi, "").trim();
|
|
41831
42447
|
if (kids) liftedParts.push(kids);
|
|
41832
42448
|
}
|
|
41833
|
-
bodyHTML = bodyHTML.replace(/<head[^>]
|
|
42449
|
+
bodyHTML = bodyHTML.replace(/<head(?:\s[^>]*)?>[\s\S]*?<\/head\s*>/gi, "");
|
|
42450
|
+
const templateTitle = collectedTitles.find((t2) => t2.includes("__TW_PAGETITLE__"));
|
|
42451
|
+
layoutTitle = templateTitle ?? collectedTitles.filter(Boolean).pop();
|
|
41834
42452
|
bodyHTML = bodyHTML.replace(/<\/?(?:html|body)[^>]*>/gi, "");
|
|
41835
42453
|
bodyHTML = bodyHTML.replace(/<!DOCTYPE[^>]*>/gi, "");
|
|
41836
42454
|
}
|
|
41837
42455
|
const liftedHead = liftedParts.join("\n");
|
|
41838
|
-
|
|
42456
|
+
const liveRenderMode = this.extractRenderMode(ctx.page);
|
|
42457
|
+
if (ctx.error && (liveRenderMode === "ssr" || liveRenderMode === "stream")) {
|
|
41839
42458
|
const errorCompiled = this.compileFile(ctx.error);
|
|
41840
42459
|
allCSS = errorCompiled.css + "\n" + allCSS;
|
|
41841
42460
|
bodyHTML = `<div data-error-boundary>${bodyHTML}</div>`;
|
|
41842
42461
|
}
|
|
41843
|
-
|
|
42462
|
+
const needsSkeleton = bodyHTML.includes("data-tw-suspense") || bodyHTML.includes("data-tw-ppr");
|
|
42463
|
+
if (ctx.loading && needsSkeleton) {
|
|
41844
42464
|
const loadingCompiled = this.compileFile(ctx.loading);
|
|
41845
42465
|
allCSS = loadingCompiled.css + "\n" + allCSS;
|
|
41846
42466
|
let lh = String(loadingCompiled.html || "");
|
|
@@ -41852,7 +42472,7 @@ ${bodyHTML}`;
|
|
|
41852
42472
|
const pageTitle = this.extractTitle(ctx.page) || "TW Page";
|
|
41853
42473
|
let title = pageTitle;
|
|
41854
42474
|
if (layoutTitle !== void 0) {
|
|
41855
|
-
title = layoutTitle.replace(/\{\s*page\.title\s*\}/g, pageTitle);
|
|
42475
|
+
title = layoutTitle.replace(/__TW_PAGETITLE__/g, pageTitle).replace(/\{\s*page\.title\s*\}/g, pageTitle);
|
|
41856
42476
|
if (!title.trim()) title = pageTitle;
|
|
41857
42477
|
}
|
|
41858
42478
|
const metaTags = this.buildMetaTags(ctx.page);
|
|
@@ -41872,7 +42492,9 @@ ${bodyHTML}`;
|
|
|
41872
42492
|
} catch {
|
|
41873
42493
|
}
|
|
41874
42494
|
let fullHTML = this.assembleHTML(bodyHTML, allCSS, allJS, title, metaTags + headExtra);
|
|
41875
|
-
|
|
42495
|
+
const pageRenderMode = this.extractRenderMode(ctx.page);
|
|
42496
|
+
const needsClientRuntime = /data-tw-event|data-tw-i|__TW_SIGNALS__|data-tw-suspense|data-tw-ppr/.test(fullHTML) || pageRenderMode === "csr" || pageRenderMode === "stream" || pageRenderMode === "ppr" || pageRenderMode === "signalStream";
|
|
42497
|
+
if (needsClientRuntime && !/__tw_runtime\.js/.test(fullHTML)) {
|
|
41876
42498
|
let stateSeed = "{}";
|
|
41877
42499
|
try {
|
|
41878
42500
|
stateSeed = JSON.stringify(pageState ?? {});
|
|
@@ -41991,7 +42613,8 @@ ${bodyHTML}`;
|
|
|
41991
42613
|
throw new Error(`Route file not found: ${file.absolutePath}`);
|
|
41992
42614
|
}
|
|
41993
42615
|
const source = (0, import_node_fs6.readFileSync)(file.absolutePath, "utf-8");
|
|
41994
|
-
const
|
|
42616
|
+
const srcWithSentinel = source.replace(/\{\s*page\.title\s*\}/g, "__TW_PAGETITLE__");
|
|
42617
|
+
const result = compileSync(srcWithSentinel, {
|
|
41995
42618
|
filePath: file.absolutePath,
|
|
41996
42619
|
optimize: true,
|
|
41997
42620
|
diagnostics: false,
|
|
@@ -42065,12 +42688,13 @@ ${bodyHTML}`;
|
|
|
42065
42688
|
try {
|
|
42066
42689
|
const source = (0, import_node_fs6.readFileSync)(file.absolutePath, "utf-8");
|
|
42067
42690
|
const masked = maskSourceStringsAndComments(source);
|
|
42068
|
-
const m = masked.match(/page\s*\{[^}]*title\s
|
|
42691
|
+
const m = masked.match(/page\s*\{[^}]*title\s/);
|
|
42069
42692
|
if (!m || m.index === void 0) return null;
|
|
42070
42693
|
const start = m.index + m[0].length;
|
|
42071
|
-
|
|
42072
|
-
|
|
42073
|
-
|
|
42694
|
+
if (source[start] !== '"') return null;
|
|
42695
|
+
const end = source.indexOf('"', start + 1);
|
|
42696
|
+
if (end === -1 || end === start + 1) return null;
|
|
42697
|
+
return source.slice(start + 1, end).replace(/\\([{}"])/g, "$1");
|
|
42074
42698
|
} catch {
|
|
42075
42699
|
return null;
|
|
42076
42700
|
}
|
|
@@ -42119,7 +42743,8 @@ ${bodyHTML}`;
|
|
|
42119
42743
|
* render-cache never reads `stale`.
|
|
42120
42744
|
*/
|
|
42121
42745
|
stampCacheHeaders(headers, entry, now, verdict) {
|
|
42122
|
-
const out = { ...headers ?? {}
|
|
42746
|
+
const out = { ...headers ?? {} };
|
|
42747
|
+
if (process.env.TW_DEBUG_CACHE || process.env.NODE_ENV === "test") out["x-tw-cache"] = verdict;
|
|
42123
42748
|
const c = entry?.cache;
|
|
42124
42749
|
if (c) {
|
|
42125
42750
|
const ageSec = entry?.createdAt ? Math.max(0, Math.floor((now - entry.createdAt) / 1e3)) : 0;
|
|
@@ -43653,7 +44278,6 @@ function securityHeadersMiddleware() {
|
|
|
43653
44278
|
return (req, res, next) => {
|
|
43654
44279
|
res.headers.set("X-Content-Type-Options", "nosniff");
|
|
43655
44280
|
res.headers.set("X-Frame-Options", "SAMEORIGIN");
|
|
43656
|
-
res.headers.set("X-XSS-Protection", "1; mode=block");
|
|
43657
44281
|
res.headers.set("X-Permitted-Cross-Domain-Policies", "none");
|
|
43658
44282
|
res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
43659
44283
|
res.headers.set("Cross-Origin-Opener-Policy", "same-origin");
|
|
@@ -43830,13 +44454,15 @@ function compressionMiddleware() {
|
|
|
43830
44454
|
await next();
|
|
43831
44455
|
};
|
|
43832
44456
|
}
|
|
43833
|
-
function securityHeadersMiddleware2() {
|
|
44457
|
+
function securityHeadersMiddleware2(opts) {
|
|
43834
44458
|
return async (ctx, next) => {
|
|
43835
44459
|
ctx.headers["x-content-type-options"] = "nosniff";
|
|
43836
44460
|
ctx.headers["x-frame-options"] = "DENY";
|
|
43837
|
-
ctx.headers["x-xss-protection"] = "1; mode=block";
|
|
43838
44461
|
ctx.headers["referrer-policy"] = "strict-origin-when-cross-origin";
|
|
43839
44462
|
ctx.headers["permissions-policy"] = "camera=(), microphone=(), geolocation=()";
|
|
44463
|
+
if (opts?.csp !== false && !ctx.headers["content-security-policy"]) {
|
|
44464
|
+
ctx.headers["content-security-policy"] = typeof opts?.csp === "string" ? opts.csp : "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'";
|
|
44465
|
+
}
|
|
43840
44466
|
await next();
|
|
43841
44467
|
};
|
|
43842
44468
|
}
|
|
@@ -43919,6 +44545,161 @@ var init_middleware2 = __esm({
|
|
|
43919
44545
|
}
|
|
43920
44546
|
});
|
|
43921
44547
|
|
|
44548
|
+
// ../../packages/server/tw/compression.ts
|
|
44549
|
+
function shouldCompress(contentType, options = {}) {
|
|
44550
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44551
|
+
const ct = contentType.split(";")[0].trim();
|
|
44552
|
+
return opts.contentTypes.includes(ct);
|
|
44553
|
+
}
|
|
44554
|
+
function isLargeEnough(size, options = {}) {
|
|
44555
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44556
|
+
return size >= opts.threshold;
|
|
44557
|
+
}
|
|
44558
|
+
async function compress(data, encoding = "gzip", options = {}) {
|
|
44559
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44560
|
+
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
44561
|
+
if (input.length < opts.threshold) {
|
|
44562
|
+
return input;
|
|
44563
|
+
}
|
|
44564
|
+
try {
|
|
44565
|
+
if (typeof Bun !== "undefined" && Bun.gzipSync) {
|
|
44566
|
+
if (encoding === "gzip") return Bun.gzipSync(input, { level: opts.level });
|
|
44567
|
+
if (encoding === "deflate" && Bun.deflateSync) return Bun.deflateSync(input, { level: opts.level });
|
|
44568
|
+
}
|
|
44569
|
+
} catch {
|
|
44570
|
+
}
|
|
44571
|
+
try {
|
|
44572
|
+
const zlib = await import("node:zlib");
|
|
44573
|
+
if (encoding === "gzip") {
|
|
44574
|
+
return zlib.gzipSync(input, { level: opts.level });
|
|
44575
|
+
}
|
|
44576
|
+
if (encoding === "deflate") {
|
|
44577
|
+
return zlib.deflateSync(input, { level: opts.level });
|
|
44578
|
+
}
|
|
44579
|
+
if (encoding === "brotli" && zlib.brotliCompressSync) {
|
|
44580
|
+
return zlib.brotliCompressSync(input, {
|
|
44581
|
+
params: {
|
|
44582
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: opts.level
|
|
44583
|
+
}
|
|
44584
|
+
});
|
|
44585
|
+
}
|
|
44586
|
+
} catch {
|
|
44587
|
+
}
|
|
44588
|
+
return input;
|
|
44589
|
+
}
|
|
44590
|
+
async function decompress(data, encoding = "gzip") {
|
|
44591
|
+
try {
|
|
44592
|
+
if (typeof Bun !== "undefined" && Bun.gunzipSync) {
|
|
44593
|
+
if (encoding === "gzip") return Bun.gunzipSync(data);
|
|
44594
|
+
if (encoding === "deflate" && Bun.inflateSync) return Bun.inflateSync(data);
|
|
44595
|
+
}
|
|
44596
|
+
} catch {
|
|
44597
|
+
}
|
|
44598
|
+
try {
|
|
44599
|
+
const zlib = await import("node:zlib");
|
|
44600
|
+
if (encoding === "gzip") return zlib.gunzipSync(data);
|
|
44601
|
+
if (encoding === "deflate") return zlib.inflateSync(data);
|
|
44602
|
+
if (encoding === "brotli" && zlib.brotliDecompressSync) return zlib.brotliDecompressSync(data);
|
|
44603
|
+
} catch {
|
|
44604
|
+
}
|
|
44605
|
+
return data;
|
|
44606
|
+
}
|
|
44607
|
+
function negotiateEncoding(acceptEncoding) {
|
|
44608
|
+
const encodings = acceptEncoding.split(",").map((e) => e.trim().split(";")[0].trim()).filter(Boolean);
|
|
44609
|
+
if (encodings.includes("br")) return "brotli";
|
|
44610
|
+
if (encodings.includes("gzip")) return "gzip";
|
|
44611
|
+
if (encodings.includes("deflate")) return "deflate";
|
|
44612
|
+
return null;
|
|
44613
|
+
}
|
|
44614
|
+
function createCompressionHeaders(encoding, originalSize, compressedSize) {
|
|
44615
|
+
return {
|
|
44616
|
+
"Content-Encoding": encoding === "brotli" ? "br" : encoding,
|
|
44617
|
+
"Vary": "Accept-Encoding",
|
|
44618
|
+
"X-Original-Size": String(originalSize),
|
|
44619
|
+
"X-Compressed-Size": String(compressedSize),
|
|
44620
|
+
"X-Compression-Ratio": originalSize > 0 ? `${((1 - compressedSize / originalSize) * 100).toFixed(1)}%` : "0%"
|
|
44621
|
+
};
|
|
44622
|
+
}
|
|
44623
|
+
async function compressResponse(body, contentType, acceptEncoding, options = {}) {
|
|
44624
|
+
const input = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
44625
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44626
|
+
if (!shouldCompress(contentType, opts) || !isLargeEnough(input.length, opts)) {
|
|
44627
|
+
return { body: input, headers: {}, compressed: false };
|
|
44628
|
+
}
|
|
44629
|
+
const encoding = negotiateEncoding(acceptEncoding);
|
|
44630
|
+
if (!encoding) {
|
|
44631
|
+
return { body: input, headers: {}, compressed: false };
|
|
44632
|
+
}
|
|
44633
|
+
try {
|
|
44634
|
+
const compressed = await compress(input, encoding, opts);
|
|
44635
|
+
const headers = createCompressionHeaders(encoding, input.length, compressed.length);
|
|
44636
|
+
return { body: compressed, headers, compressed: true };
|
|
44637
|
+
} catch {
|
|
44638
|
+
return { body: input, headers: {}, compressed: false };
|
|
44639
|
+
}
|
|
44640
|
+
}
|
|
44641
|
+
var DEFAULT_OPTIONS4, StreamingCompressor;
|
|
44642
|
+
var init_compression = __esm({
|
|
44643
|
+
"../../packages/server/tw/compression.ts"() {
|
|
44644
|
+
DEFAULT_OPTIONS4 = {
|
|
44645
|
+
level: 6,
|
|
44646
|
+
threshold: 1024,
|
|
44647
|
+
// Don't compress < 1KB
|
|
44648
|
+
brotli: true,
|
|
44649
|
+
contentTypes: [
|
|
44650
|
+
"text/html",
|
|
44651
|
+
"text/css",
|
|
44652
|
+
"text/plain",
|
|
44653
|
+
"text/javascript",
|
|
44654
|
+
"application/javascript",
|
|
44655
|
+
"application/json",
|
|
44656
|
+
"application/xml",
|
|
44657
|
+
"application/xhtml+xml",
|
|
44658
|
+
"image/svg+xml",
|
|
44659
|
+
"font/woff",
|
|
44660
|
+
"font/woff2",
|
|
44661
|
+
"application/wasm"
|
|
44662
|
+
]
|
|
44663
|
+
};
|
|
44664
|
+
StreamingCompressor = class {
|
|
44665
|
+
chunks = [];
|
|
44666
|
+
encoding;
|
|
44667
|
+
options;
|
|
44668
|
+
constructor(encoding = "gzip", options = {}) {
|
|
44669
|
+
this.encoding = encoding;
|
|
44670
|
+
this.options = { ...DEFAULT_OPTIONS4, ...options };
|
|
44671
|
+
}
|
|
44672
|
+
/**
|
|
44673
|
+
* Add a chunk of data to be compressed.
|
|
44674
|
+
*/
|
|
44675
|
+
push(data) {
|
|
44676
|
+
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
44677
|
+
this.chunks.push(input);
|
|
44678
|
+
}
|
|
44679
|
+
/**
|
|
44680
|
+
* Flush all data and return compressed result.
|
|
44681
|
+
*/
|
|
44682
|
+
async flush() {
|
|
44683
|
+
const totalLength = this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
44684
|
+
const combined = new Uint8Array(totalLength);
|
|
44685
|
+
let offset = 0;
|
|
44686
|
+
for (const chunk of this.chunks) {
|
|
44687
|
+
combined.set(chunk, offset);
|
|
44688
|
+
offset += chunk.length;
|
|
44689
|
+
}
|
|
44690
|
+
this.chunks = [];
|
|
44691
|
+
return compress(combined, this.encoding, this.options);
|
|
44692
|
+
}
|
|
44693
|
+
/**
|
|
44694
|
+
* Get the total uncompressed size so far.
|
|
44695
|
+
*/
|
|
44696
|
+
get uncompressedSize() {
|
|
44697
|
+
return this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
44698
|
+
}
|
|
44699
|
+
};
|
|
44700
|
+
}
|
|
44701
|
+
});
|
|
44702
|
+
|
|
43922
44703
|
// ../../packages/server/tw/static/index.ts
|
|
43923
44704
|
var import_node_fs8, import_node_path15, StaticHandler;
|
|
43924
44705
|
var init_static = __esm({
|
|
@@ -44128,7 +44909,7 @@ var init_policy = __esm({
|
|
|
44128
44909
|
"Content-Security-Policy": this.build(),
|
|
44129
44910
|
"X-Content-Type-Options": "nosniff",
|
|
44130
44911
|
"X-Frame-Options": "DENY",
|
|
44131
|
-
|
|
44912
|
+
// BUG 48: x-xss-protection removed (deprecated, harmful in old browsers)
|
|
44132
44913
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
44133
44914
|
"Permissions-Policy": "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()",
|
|
44134
44915
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
@@ -48762,9 +49543,10 @@ function devSecurityHeaders() {
|
|
|
48762
49543
|
removePoweredBy: true
|
|
48763
49544
|
});
|
|
48764
49545
|
}
|
|
48765
|
-
var DEFAULTS2, SecurityHeaders2;
|
|
49546
|
+
var BASELINE_CSP, DEFAULTS2, SecurityHeaders2;
|
|
48766
49547
|
var init_security_headers = __esm({
|
|
48767
49548
|
"../../packages/security/tw/headers/security-headers.ts"() {
|
|
49549
|
+
BASELINE_CSP = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'";
|
|
48768
49550
|
DEFAULTS2 = {
|
|
48769
49551
|
hstsMaxAge: 31536e3,
|
|
48770
49552
|
// 1 year
|
|
@@ -48773,7 +49555,9 @@ var init_security_headers = __esm({
|
|
|
48773
49555
|
frameOptions: "SAMEORIGIN",
|
|
48774
49556
|
contentTypeOptions: true,
|
|
48775
49557
|
referrerPolicy: "strict-origin-when-cross-origin",
|
|
48776
|
-
|
|
49558
|
+
// v1.0.8 round 5 (BUG 48): x-xss-protection deprecated since 2019 --
|
|
49559
|
+
// OFF by default now; opt back in with security.xssProtection.
|
|
49560
|
+
xssProtection: void 0,
|
|
48777
49561
|
removePoweredBy: true,
|
|
48778
49562
|
coep: "unsafe-none",
|
|
48779
49563
|
coop: "same-origin",
|
|
@@ -48829,6 +49613,9 @@ var init_security_headers = __esm({
|
|
|
48829
49613
|
if (this.config.serverHeader) {
|
|
48830
49614
|
headers["Server"] = this.config.serverHeader;
|
|
48831
49615
|
}
|
|
49616
|
+
if (this.config.csp !== false && !headers["Content-Security-Policy"]) {
|
|
49617
|
+
headers["Content-Security-Policy"] = typeof this.config.csp === "string" ? this.config.csp : BASELINE_CSP;
|
|
49618
|
+
}
|
|
48832
49619
|
return headers;
|
|
48833
49620
|
}
|
|
48834
49621
|
/** Returns the security headers object. */
|
|
@@ -53039,161 +53826,6 @@ var init_ssr = __esm({
|
|
|
53039
53826
|
}
|
|
53040
53827
|
});
|
|
53041
53828
|
|
|
53042
|
-
// ../../packages/server/tw/compression.ts
|
|
53043
|
-
function shouldCompress(contentType, options = {}) {
|
|
53044
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53045
|
-
const ct = contentType.split(";")[0].trim();
|
|
53046
|
-
return opts.contentTypes.includes(ct);
|
|
53047
|
-
}
|
|
53048
|
-
function isLargeEnough(size, options = {}) {
|
|
53049
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53050
|
-
return size >= opts.threshold;
|
|
53051
|
-
}
|
|
53052
|
-
async function compress(data, encoding = "gzip", options = {}) {
|
|
53053
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53054
|
-
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
53055
|
-
if (input.length < opts.threshold) {
|
|
53056
|
-
return input;
|
|
53057
|
-
}
|
|
53058
|
-
try {
|
|
53059
|
-
if (typeof Bun !== "undefined" && Bun.gzipSync) {
|
|
53060
|
-
if (encoding === "gzip") return Bun.gzipSync(input, { level: opts.level });
|
|
53061
|
-
if (encoding === "deflate" && Bun.deflateSync) return Bun.deflateSync(input, { level: opts.level });
|
|
53062
|
-
}
|
|
53063
|
-
} catch {
|
|
53064
|
-
}
|
|
53065
|
-
try {
|
|
53066
|
-
const zlib = await import("node:zlib");
|
|
53067
|
-
if (encoding === "gzip") {
|
|
53068
|
-
return zlib.gzipSync(input, { level: opts.level });
|
|
53069
|
-
}
|
|
53070
|
-
if (encoding === "deflate") {
|
|
53071
|
-
return zlib.deflateSync(input, { level: opts.level });
|
|
53072
|
-
}
|
|
53073
|
-
if (encoding === "brotli" && zlib.brotliCompressSync) {
|
|
53074
|
-
return zlib.brotliCompressSync(input, {
|
|
53075
|
-
params: {
|
|
53076
|
-
[zlib.constants.BROTLI_PARAM_QUALITY]: opts.level
|
|
53077
|
-
}
|
|
53078
|
-
});
|
|
53079
|
-
}
|
|
53080
|
-
} catch {
|
|
53081
|
-
}
|
|
53082
|
-
return input;
|
|
53083
|
-
}
|
|
53084
|
-
async function decompress(data, encoding = "gzip") {
|
|
53085
|
-
try {
|
|
53086
|
-
if (typeof Bun !== "undefined" && Bun.gunzipSync) {
|
|
53087
|
-
if (encoding === "gzip") return Bun.gunzipSync(data);
|
|
53088
|
-
if (encoding === "deflate" && Bun.inflateSync) return Bun.inflateSync(data);
|
|
53089
|
-
}
|
|
53090
|
-
} catch {
|
|
53091
|
-
}
|
|
53092
|
-
try {
|
|
53093
|
-
const zlib = await import("node:zlib");
|
|
53094
|
-
if (encoding === "gzip") return zlib.gunzipSync(data);
|
|
53095
|
-
if (encoding === "deflate") return zlib.inflateSync(data);
|
|
53096
|
-
if (encoding === "brotli" && zlib.brotliDecompressSync) return zlib.brotliDecompressSync(data);
|
|
53097
|
-
} catch {
|
|
53098
|
-
}
|
|
53099
|
-
return data;
|
|
53100
|
-
}
|
|
53101
|
-
function negotiateEncoding(acceptEncoding) {
|
|
53102
|
-
const encodings = acceptEncoding.split(",").map((e) => e.trim().split(";")[0].trim()).filter(Boolean);
|
|
53103
|
-
if (encodings.includes("br")) return "brotli";
|
|
53104
|
-
if (encodings.includes("gzip")) return "gzip";
|
|
53105
|
-
if (encodings.includes("deflate")) return "deflate";
|
|
53106
|
-
return null;
|
|
53107
|
-
}
|
|
53108
|
-
function createCompressionHeaders(encoding, originalSize, compressedSize) {
|
|
53109
|
-
return {
|
|
53110
|
-
"Content-Encoding": encoding === "brotli" ? "br" : encoding,
|
|
53111
|
-
"Vary": "Accept-Encoding",
|
|
53112
|
-
"X-Original-Size": String(originalSize),
|
|
53113
|
-
"X-Compressed-Size": String(compressedSize),
|
|
53114
|
-
"X-Compression-Ratio": originalSize > 0 ? `${((1 - compressedSize / originalSize) * 100).toFixed(1)}%` : "0%"
|
|
53115
|
-
};
|
|
53116
|
-
}
|
|
53117
|
-
async function compressResponse(body, contentType, acceptEncoding, options = {}) {
|
|
53118
|
-
const input = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
53119
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53120
|
-
if (!shouldCompress(contentType, opts) || !isLargeEnough(input.length, opts)) {
|
|
53121
|
-
return { body: input, headers: {}, compressed: false };
|
|
53122
|
-
}
|
|
53123
|
-
const encoding = negotiateEncoding(acceptEncoding);
|
|
53124
|
-
if (!encoding) {
|
|
53125
|
-
return { body: input, headers: {}, compressed: false };
|
|
53126
|
-
}
|
|
53127
|
-
try {
|
|
53128
|
-
const compressed = await compress(input, encoding, opts);
|
|
53129
|
-
const headers = createCompressionHeaders(encoding, input.length, compressed.length);
|
|
53130
|
-
return { body: compressed, headers, compressed: true };
|
|
53131
|
-
} catch {
|
|
53132
|
-
return { body: input, headers: {}, compressed: false };
|
|
53133
|
-
}
|
|
53134
|
-
}
|
|
53135
|
-
var DEFAULT_OPTIONS4, StreamingCompressor;
|
|
53136
|
-
var init_compression = __esm({
|
|
53137
|
-
"../../packages/server/tw/compression.ts"() {
|
|
53138
|
-
DEFAULT_OPTIONS4 = {
|
|
53139
|
-
level: 6,
|
|
53140
|
-
threshold: 1024,
|
|
53141
|
-
// Don't compress < 1KB
|
|
53142
|
-
brotli: true,
|
|
53143
|
-
contentTypes: [
|
|
53144
|
-
"text/html",
|
|
53145
|
-
"text/css",
|
|
53146
|
-
"text/plain",
|
|
53147
|
-
"text/javascript",
|
|
53148
|
-
"application/javascript",
|
|
53149
|
-
"application/json",
|
|
53150
|
-
"application/xml",
|
|
53151
|
-
"application/xhtml+xml",
|
|
53152
|
-
"image/svg+xml",
|
|
53153
|
-
"font/woff",
|
|
53154
|
-
"font/woff2",
|
|
53155
|
-
"application/wasm"
|
|
53156
|
-
]
|
|
53157
|
-
};
|
|
53158
|
-
StreamingCompressor = class {
|
|
53159
|
-
chunks = [];
|
|
53160
|
-
encoding;
|
|
53161
|
-
options;
|
|
53162
|
-
constructor(encoding = "gzip", options = {}) {
|
|
53163
|
-
this.encoding = encoding;
|
|
53164
|
-
this.options = { ...DEFAULT_OPTIONS4, ...options };
|
|
53165
|
-
}
|
|
53166
|
-
/**
|
|
53167
|
-
* Add a chunk of data to be compressed.
|
|
53168
|
-
*/
|
|
53169
|
-
push(data) {
|
|
53170
|
-
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
53171
|
-
this.chunks.push(input);
|
|
53172
|
-
}
|
|
53173
|
-
/**
|
|
53174
|
-
* Flush all data and return compressed result.
|
|
53175
|
-
*/
|
|
53176
|
-
async flush() {
|
|
53177
|
-
const totalLength = this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
53178
|
-
const combined = new Uint8Array(totalLength);
|
|
53179
|
-
let offset = 0;
|
|
53180
|
-
for (const chunk of this.chunks) {
|
|
53181
|
-
combined.set(chunk, offset);
|
|
53182
|
-
offset += chunk.length;
|
|
53183
|
-
}
|
|
53184
|
-
this.chunks = [];
|
|
53185
|
-
return compress(combined, this.encoding, this.options);
|
|
53186
|
-
}
|
|
53187
|
-
/**
|
|
53188
|
-
* Get the total uncompressed size so far.
|
|
53189
|
-
*/
|
|
53190
|
-
get uncompressedSize() {
|
|
53191
|
-
return this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
53192
|
-
}
|
|
53193
|
-
};
|
|
53194
|
-
}
|
|
53195
|
-
});
|
|
53196
|
-
|
|
53197
53829
|
// ../../packages/server/tw/edge.ts
|
|
53198
53830
|
function createEdgeHandler(config) {
|
|
53199
53831
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -53912,8 +54544,11 @@ var init_tw6 = __esm({
|
|
|
53912
54544
|
init_routing2();
|
|
53913
54545
|
import_node_path17 = require("node:path");
|
|
53914
54546
|
import_node_fs11 = require("node:fs");
|
|
54547
|
+
init_twm_rules();
|
|
53915
54548
|
init_router();
|
|
53916
54549
|
init_middleware2();
|
|
54550
|
+
init_compression();
|
|
54551
|
+
init_signal_stream();
|
|
53917
54552
|
init_static();
|
|
53918
54553
|
init_tw5();
|
|
53919
54554
|
init_ssr();
|
|
@@ -53962,6 +54597,7 @@ var init_tw6 = __esm({
|
|
|
53962
54597
|
}
|
|
53963
54598
|
ssr = null;
|
|
53964
54599
|
server = null;
|
|
54600
|
+
compressionMode = "off";
|
|
53965
54601
|
isRunning = false;
|
|
53966
54602
|
pluginManager = null;
|
|
53967
54603
|
imageHandler = null;
|
|
@@ -53978,6 +54614,40 @@ var init_tw6 = __esm({
|
|
|
53978
54614
|
const entries = Object.entries(cfgIn.redirects);
|
|
53979
54615
|
cfgIn.redirects = entries.map(([from, to]) => typeof to === "string" ? { from, to } : { from, ...to });
|
|
53980
54616
|
}
|
|
54617
|
+
if (Array.isArray(cfgIn.redirects)) {
|
|
54618
|
+
const REDIRECT_KNOWN = /* @__PURE__ */ new Set(["from", "to", "status", "source", "destination", "permanent"]);
|
|
54619
|
+
let redirectDropped = false;
|
|
54620
|
+
const normalizedRedirects = cfgIn.redirects.map((r) => {
|
|
54621
|
+
if (!r || typeof r !== "object") return r;
|
|
54622
|
+
let touched = false;
|
|
54623
|
+
const mapped = { ...r };
|
|
54624
|
+
if (mapped.from === void 0 && mapped.source !== void 0) {
|
|
54625
|
+
mapped.from = mapped.source;
|
|
54626
|
+
touched = true;
|
|
54627
|
+
}
|
|
54628
|
+
if (mapped.to === void 0 && mapped.destination !== void 0) {
|
|
54629
|
+
mapped.to = mapped.destination;
|
|
54630
|
+
touched = true;
|
|
54631
|
+
}
|
|
54632
|
+
if (mapped.status === void 0 && mapped.permanent !== void 0) {
|
|
54633
|
+
mapped.status = mapped.permanent ? 301 : 302;
|
|
54634
|
+
touched = true;
|
|
54635
|
+
}
|
|
54636
|
+
const unknown = Object.keys(mapped).filter((k) => !REDIRECT_KNOWN.has(k));
|
|
54637
|
+
if (unknown.length > 0) {
|
|
54638
|
+
console.warn("[tw] redirects: unrecognized field" + (unknown.length > 1 ? "s" : "") + " " + unknown.join(", ") + " in entry { from: " + JSON.stringify(mapped.from) + " } -- accepted fields: from, to, status");
|
|
54639
|
+
}
|
|
54640
|
+
if (typeof mapped.from !== "string" || typeof mapped.to !== "string") {
|
|
54641
|
+
console.warn("[tw] redirects: entry missing from/to after normalization -- skipped (" + JSON.stringify(r).slice(0, 80) + ")");
|
|
54642
|
+
redirectDropped = true;
|
|
54643
|
+
return null;
|
|
54644
|
+
}
|
|
54645
|
+
return touched ? mapped : r;
|
|
54646
|
+
}).filter((x) => x !== null || (redirectDropped = true));
|
|
54647
|
+
if (redirectDropped || normalizedRedirects.some((x, i) => x !== cfgIn.redirects[i])) {
|
|
54648
|
+
cfgIn.redirects = normalizedRedirects.filter(Boolean);
|
|
54649
|
+
}
|
|
54650
|
+
}
|
|
53981
54651
|
if (cfgIn.headers && !Array.isArray(cfgIn.headers)) {
|
|
53982
54652
|
const entries = Object.entries(cfgIn.headers);
|
|
53983
54653
|
cfgIn.headers = entries.map(([source, headers]) => ({ source, headers }));
|
|
@@ -53995,6 +54665,12 @@ var init_tw6 = __esm({
|
|
|
53995
54665
|
}
|
|
53996
54666
|
}
|
|
53997
54667
|
this.pluginManager = this.options.plugins ?? null;
|
|
54668
|
+
{
|
|
54669
|
+
const raw = this.options.compression ?? true;
|
|
54670
|
+
if (raw === false || raw === "none" || raw === "off" || raw === "false") this.compressionMode = "off";
|
|
54671
|
+
else if (raw === "gzip") this.compressionMode = "gzip";
|
|
54672
|
+
else this.compressionMode = "brotli";
|
|
54673
|
+
}
|
|
53998
54674
|
try {
|
|
53999
54675
|
this.imageHandler = createImageHandler({
|
|
54000
54676
|
rootDir: this.options.rootDir,
|
|
@@ -54104,31 +54780,72 @@ var init_tw6 = __esm({
|
|
|
54104
54780
|
if ((0, import_node_fs11.existsSync)(middlewarePath)) {
|
|
54105
54781
|
this.middlewarePath = middlewarePath;
|
|
54106
54782
|
console.log(" Middleware: middleware.twm loaded");
|
|
54783
|
+
try {
|
|
54784
|
+
const src = (0, import_node_fs11.readFileSync)(middlewarePath, "utf-8");
|
|
54785
|
+
const rules = parseRules(src, "middleware.twm");
|
|
54786
|
+
for (const r of rules) {
|
|
54787
|
+
const conds = [];
|
|
54788
|
+
if (r.userAgent) conds.push("user_agent");
|
|
54789
|
+
if (r.path) conds.push("path");
|
|
54790
|
+
if (r.auth) conds.push("auth");
|
|
54791
|
+
if (r.rateLimit) conds.push("rate_limit");
|
|
54792
|
+
if (r.origin) conds.push("origin");
|
|
54793
|
+
if (r.methods && r.methods.length > 0) conds.push("methods");
|
|
54794
|
+
const label = conds.length > 0 ? conds.join(" + ") : "no conditions -> every matching request gets this response";
|
|
54795
|
+
console.log(` \u2022 "${r.name}" [${label}]`);
|
|
54796
|
+
}
|
|
54797
|
+
} catch {
|
|
54798
|
+
}
|
|
54107
54799
|
}
|
|
54108
54800
|
return this;
|
|
54109
54801
|
}
|
|
54110
54802
|
async start() {
|
|
54111
54803
|
if (this.isRunning) return;
|
|
54112
|
-
const
|
|
54113
|
-
|
|
54114
|
-
|
|
54115
|
-
|
|
54116
|
-
|
|
54117
|
-
|
|
54118
|
-
|
|
54119
|
-
|
|
54120
|
-
|
|
54121
|
-
|
|
54122
|
-
|
|
54804
|
+
const host = this.options.host ?? "0.0.0.0";
|
|
54805
|
+
const wanted = this.options.port ?? 8e3;
|
|
54806
|
+
let port = wanted;
|
|
54807
|
+
let bound = null;
|
|
54808
|
+
let lastErr = null;
|
|
54809
|
+
for (let p = wanted; p < wanted + 10; p++) {
|
|
54810
|
+
try {
|
|
54811
|
+
bound = (typeof Bun !== "undefined" ? Bun : null)?.serve({
|
|
54812
|
+
port: p,
|
|
54813
|
+
hostname: host,
|
|
54814
|
+
fetch: async (request) => {
|
|
54815
|
+
const res = await this.handleRequest(request);
|
|
54816
|
+
let withHeaders = res;
|
|
54817
|
+
if (this.securityHeaders) {
|
|
54818
|
+
try {
|
|
54819
|
+
withHeaders = this.securityHeaders.apply(res);
|
|
54820
|
+
} catch {
|
|
54821
|
+
withHeaders = res;
|
|
54822
|
+
}
|
|
54823
|
+
}
|
|
54824
|
+
return this.compressResponse(withHeaders, request);
|
|
54825
|
+
},
|
|
54826
|
+
error: (err3) => {
|
|
54827
|
+
console.error("Server error:", err3);
|
|
54828
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
54123
54829
|
}
|
|
54124
|
-
}
|
|
54125
|
-
|
|
54126
|
-
|
|
54127
|
-
|
|
54128
|
-
|
|
54129
|
-
|
|
54830
|
+
});
|
|
54831
|
+
port = p;
|
|
54832
|
+
break;
|
|
54833
|
+
} catch (e) {
|
|
54834
|
+
lastErr = e;
|
|
54835
|
+
const msg = String(e?.message ?? e);
|
|
54836
|
+
if (!/address already in use|EADDRINUSE|Failed to start server/i.test(msg)) throw e;
|
|
54837
|
+
if (p < wanted + 9) console.log(` Port ${p} busy \u2014 trying ${p + 1}...`);
|
|
54130
54838
|
}
|
|
54131
|
-
}
|
|
54839
|
+
}
|
|
54840
|
+
if (!bound) {
|
|
54841
|
+
console.error(`
|
|
54842
|
+
ERROR: could not bind ${host} on ports ${wanted}\u2013${wanted + 9}.`);
|
|
54843
|
+
console.error(` ${lastErr?.message ?? lastErr}
|
|
54844
|
+
`);
|
|
54845
|
+
throw lastErr ?? new Error("could not start server");
|
|
54846
|
+
}
|
|
54847
|
+
this.server = bound;
|
|
54848
|
+
this.options.port = port;
|
|
54132
54849
|
this.isRunning = true;
|
|
54133
54850
|
console.log(`
|
|
54134
54851
|
TW Server running at http://${host}:${port}
|
|
@@ -54142,6 +54859,10 @@ var init_tw6 = __esm({
|
|
|
54142
54859
|
// finishes ongoing requests, then closes
|
|
54143
54860
|
async gracefulShutdown(timeout = 5e3) {
|
|
54144
54861
|
this.isRunning = false;
|
|
54862
|
+
try {
|
|
54863
|
+
getSignalHub().closeAll();
|
|
54864
|
+
} catch {
|
|
54865
|
+
}
|
|
54145
54866
|
if (this.server) {
|
|
54146
54867
|
this.server.stop?.(false);
|
|
54147
54868
|
const start = Date.now();
|
|
@@ -54167,6 +54888,35 @@ var init_tw6 = __esm({
|
|
|
54167
54888
|
* - return { headers: {...} } -> headers merged into the final response
|
|
54168
54889
|
* - return null/undefined -> continue
|
|
54169
54890
|
*/
|
|
54891
|
+
/** BUG 7 (v1.0.8): real response compression. `compression` in
|
|
54892
|
+
* tw.config.ts was silently ignored -- the middleware was a no-op and
|
|
54893
|
+
* nothing on the response path ever set Content-Encoding. */
|
|
54894
|
+
async compressResponse(res, request) {
|
|
54895
|
+
if (this.compressionMode === "off") return res;
|
|
54896
|
+
try {
|
|
54897
|
+
if (res.headers.get("content-encoding")) return res;
|
|
54898
|
+
const ct = res.headers.get("content-type") || "";
|
|
54899
|
+
if (ct.includes("text/event-stream")) return res;
|
|
54900
|
+
if (res.__twRawStream) return res;
|
|
54901
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
54902
|
+
if (!isLargeEnough(buf.length)) return new Response(buf, { status: res.status, headers: res.headers });
|
|
54903
|
+
const accept = request.headers.get("accept-encoding") || "";
|
|
54904
|
+
let enc = negotiateEncoding(accept);
|
|
54905
|
+
if (this.compressionMode === "gzip" && enc === "brotli") enc = "gzip";
|
|
54906
|
+
if (!enc) return new Response(buf, { status: res.status, headers: res.headers });
|
|
54907
|
+
const out = await compress(buf, enc);
|
|
54908
|
+
if (out.length >= buf.length) return new Response(buf, { status: res.status, headers: res.headers });
|
|
54909
|
+
const h = new Headers(res.headers);
|
|
54910
|
+
h.set("Content-Encoding", enc === "brotli" ? "br" : enc);
|
|
54911
|
+
h.set("Vary", "Accept-Encoding");
|
|
54912
|
+
h.delete("Content-Length");
|
|
54913
|
+
return new Response(out, { status: res.status, headers: h });
|
|
54914
|
+
} catch {
|
|
54915
|
+
return res;
|
|
54916
|
+
}
|
|
54917
|
+
}
|
|
54918
|
+
/** v1.0.8 round 5 (BUG 39): public now -- serverless adapters (Vercel
|
|
54919
|
+
* functions, edge runtimes) drive the server through this entry. */
|
|
54170
54920
|
async handleRequest(request) {
|
|
54171
54921
|
try {
|
|
54172
54922
|
const u = new URL(request.url);
|
|
@@ -54339,69 +55089,12 @@ var init_tw6 = __esm({
|
|
|
54339
55089
|
* `data: {"v":1,"seq":N,"updates":[...]}` frames.
|
|
54340
55090
|
*/
|
|
54341
55091
|
handleSignalStream(request, url) {
|
|
54342
|
-
const
|
|
54343
|
-
|
|
54344
|
-
|
|
54345
|
-
|
|
54346
|
-
|
|
54347
|
-
|
|
54348
|
-
try {
|
|
54349
|
-
const header = request.headers.get("cookie") ?? "";
|
|
54350
|
-
const m = /(?:^|;\s*)tw_session=([^;]+)/.exec(header);
|
|
54351
|
-
if (m) session = decodeURIComponent(m[1].trim().replace(/^"|"$/g, ""));
|
|
54352
|
-
} catch {
|
|
54353
|
-
}
|
|
54354
|
-
const enc = new TextEncoder();
|
|
54355
|
-
const sse = (payload) => enc.encode("data: " + payload + "\n\n");
|
|
54356
|
-
const stream = new ReadableStream({
|
|
54357
|
-
start(controller) {
|
|
54358
|
-
const client = hub2.register(declared, session);
|
|
54359
|
-
client.send = (payload) => {
|
|
54360
|
-
try {
|
|
54361
|
-
controller.enqueue(sse(payload));
|
|
54362
|
-
} catch {
|
|
54363
|
-
client.close();
|
|
54364
|
-
}
|
|
54365
|
-
};
|
|
54366
|
-
client.close = () => {
|
|
54367
|
-
try {
|
|
54368
|
-
controller.close();
|
|
54369
|
-
} catch {
|
|
54370
|
-
}
|
|
54371
|
-
hub2["clients"];
|
|
54372
|
-
};
|
|
54373
|
-
for (const payload of hub2.connectPayloads(since, declared, session)) {
|
|
54374
|
-
try {
|
|
54375
|
-
controller.enqueue(sse(payload));
|
|
54376
|
-
} catch {
|
|
54377
|
-
break;
|
|
54378
|
-
}
|
|
54379
|
-
}
|
|
54380
|
-
const keepAlive = setInterval(() => {
|
|
54381
|
-
try {
|
|
54382
|
-
controller.enqueue(enc.encode(": keep-alive\n\n"));
|
|
54383
|
-
} catch {
|
|
54384
|
-
clearInterval(keepAlive);
|
|
54385
|
-
}
|
|
54386
|
-
}, 25e3);
|
|
54387
|
-
client.close = () => {
|
|
54388
|
-
clearInterval(keepAlive);
|
|
54389
|
-
try {
|
|
54390
|
-
controller.close();
|
|
54391
|
-
} catch {
|
|
54392
|
-
}
|
|
54393
|
-
};
|
|
54394
|
-
}
|
|
54395
|
-
});
|
|
54396
|
-
return new Response(stream, {
|
|
54397
|
-
status: 200,
|
|
54398
|
-
headers: {
|
|
54399
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
54400
|
-
"Cache-Control": "no-store",
|
|
54401
|
-
"X-Accel-Buffering": "no",
|
|
54402
|
-
Connection: "keep-alive"
|
|
54403
|
-
}
|
|
54404
|
-
});
|
|
55092
|
+
const cfg = this.options.config?.signalStream;
|
|
55093
|
+
return createSignalStream(request, url, cfg ? { maxClients: cfg.maxClients, maxHistory: cfg.maxHistory } : void 0);
|
|
55094
|
+
}
|
|
55095
|
+
async handleClientSignalWrite(request) {
|
|
55096
|
+
const cfg = this.options.config?.signalStream;
|
|
55097
|
+
return writeSignalFromClient(request, { clientWrites: cfg?.clientWrites });
|
|
54405
55098
|
}
|
|
54406
55099
|
/**
|
|
54407
55100
|
* PPR hole fill: render the page fresh and return only the requested
|
|
@@ -54475,15 +55168,33 @@ var init_tw6 = __esm({
|
|
|
54475
55168
|
controller.close();
|
|
54476
55169
|
}
|
|
54477
55170
|
});
|
|
54478
|
-
|
|
55171
|
+
const streamRes = new Response(stream, {
|
|
54479
55172
|
status: 200,
|
|
54480
55173
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
54481
55174
|
});
|
|
55175
|
+
streamRes.__twRawStream = true;
|
|
55176
|
+
return streamRes;
|
|
54482
55177
|
}
|
|
54483
55178
|
async handleRequestInner(request) {
|
|
54484
55179
|
const url = new URL(request.url);
|
|
54485
55180
|
const method = request.method;
|
|
54486
|
-
|
|
55181
|
+
let rawPath = url.pathname;
|
|
55182
|
+
try {
|
|
55183
|
+
const decoded = decodeURIComponent(rawPath);
|
|
55184
|
+
if (decoded !== rawPath) rawPath = decoded;
|
|
55185
|
+
} catch {
|
|
55186
|
+
}
|
|
55187
|
+
let canonical = normalizePathname(rawPath);
|
|
55188
|
+
if (canonical.length > 1 && canonical.endsWith("/")) {
|
|
55189
|
+
canonical = canonical.slice(0, -1);
|
|
55190
|
+
}
|
|
55191
|
+
if (canonical !== rawPath) {
|
|
55192
|
+
return new Response(null, {
|
|
55193
|
+
status: 308,
|
|
55194
|
+
headers: { Location: encodeURI(canonical) + url.search }
|
|
55195
|
+
});
|
|
55196
|
+
}
|
|
55197
|
+
const pathname = canonical;
|
|
54487
55198
|
if (pathname === "/_tw/img" || pathname.startsWith("/_tw/img/")) {
|
|
54488
55199
|
const imgRes = await this.imageHandler.handle(request);
|
|
54489
55200
|
if (imgRes) return imgRes;
|
|
@@ -54491,6 +55202,9 @@ var init_tw6 = __esm({
|
|
|
54491
55202
|
if (pathname === "/_tw/stream") {
|
|
54492
55203
|
return this.handleSignalStream(request, url);
|
|
54493
55204
|
}
|
|
55205
|
+
if (pathname === "/_tw/signal" && method === "POST") {
|
|
55206
|
+
return this.handleClientSignalWrite(request);
|
|
55207
|
+
}
|
|
54494
55208
|
if (pathname === "/_tw/ppr") {
|
|
54495
55209
|
return this.handlePPRHoleFill(url);
|
|
54496
55210
|
}
|
|
@@ -54655,11 +55369,37 @@ __export(twm_loader_exports, {
|
|
|
54655
55369
|
loadTWMModule: () => loadTWMModule,
|
|
54656
55370
|
revalidateRoute: () => revalidateRoute,
|
|
54657
55371
|
shouldMatchMiddleware: () => shouldMatchMiddleware,
|
|
54658
|
-
stripCommentsSafe: () => stripCommentsSafe
|
|
55372
|
+
stripCommentsSafe: () => stripCommentsSafe,
|
|
55373
|
+
validateTWMSyntax: () => validateTWMSyntax
|
|
54659
55374
|
});
|
|
54660
55375
|
function clearTWMCache() {
|
|
54661
55376
|
moduleCache.clear();
|
|
54662
55377
|
}
|
|
55378
|
+
function validateTWMSyntax(filePath) {
|
|
55379
|
+
let source;
|
|
55380
|
+
try {
|
|
55381
|
+
source = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
|
|
55382
|
+
} catch (err3) {
|
|
55383
|
+
return "cannot read: " + (err3?.message ?? err3);
|
|
55384
|
+
}
|
|
55385
|
+
if (isRuleDsl(source)) {
|
|
55386
|
+
try {
|
|
55387
|
+
parseRules(source, filePath);
|
|
55388
|
+
return null;
|
|
55389
|
+
} catch (err3) {
|
|
55390
|
+
return "rule DSL: " + (err3?.message ?? String(err3));
|
|
55391
|
+
}
|
|
55392
|
+
}
|
|
55393
|
+
try {
|
|
55394
|
+
const imports = extractImports(source);
|
|
55395
|
+
const jsCode = compileTwm(source);
|
|
55396
|
+
const codeWithoutImports = stripImports(jsCode);
|
|
55397
|
+
new Function("__tw_unused", codeWithoutImports);
|
|
55398
|
+
return null;
|
|
55399
|
+
} catch (err3) {
|
|
55400
|
+
return (err3?.message ?? String(err3)).replace(/\n/g, " ").slice(0, 200);
|
|
55401
|
+
}
|
|
55402
|
+
}
|
|
54663
55403
|
function stripCommentsSafe(src) {
|
|
54664
55404
|
let out = "";
|
|
54665
55405
|
let i = 0;
|
|
@@ -54897,7 +55637,7 @@ async function loadTWMModule(filePath, rootDir) {
|
|
|
54897
55637
|
console.error("[tw] " + e.message + " (" + filePath + ":" + cm[1] + ")");
|
|
54898
55638
|
}
|
|
54899
55639
|
}
|
|
54900
|
-
const moduleCode = codeWithoutImports + "\nreturn { get: typeof get !== 'undefined' ? get : null, post: typeof post !== 'undefined' ? post : null, put: typeof put !== 'undefined' ? put : null, patch: typeof patch !== 'undefined' ? patch : null, deleteFn: typeof deleteFn !== 'undefined' ? deleteFn : null, GET: typeof GET !== 'undefined' ? GET : null, POST: typeof POST !== 'undefined' ? POST : null, PUT: typeof PUT !== 'undefined' ? PUT : null, PATCH: typeof PATCH !== 'undefined' ? PATCH : null, DELETE: typeof DELETE !== 'undefined' ? DELETE : null, middleware: typeof middleware !== 'undefined' ? middleware : null, generate: typeof generate !== 'undefined' ? generate : null, config: typeof config !== 'undefined' ? config : null, action: typeof action !== 'undefined' ? action : null, revalidate: typeof revalidate !== 'undefined' ? revalidate : null, " + [...actionNames].map((n) => `${n}: typeof ${n} !== 'undefined' ? ${n} : null`).join(", ") + " };";
|
|
55640
|
+
const moduleCode = codeWithoutImports + "\nreturn { get: typeof get !== 'undefined' ? get : null, post: typeof post !== 'undefined' ? post : null, put: typeof put !== 'undefined' ? put : null, patch: typeof patch !== 'undefined' ? patch : null, deleteFn: typeof deleteFn !== 'undefined' ? deleteFn : null, GET: typeof GET !== 'undefined' ? GET : null, POST: typeof POST !== 'undefined' ? POST : null, PUT: typeof PUT !== 'undefined' ? PUT : null, PATCH: typeof PATCH !== 'undefined' ? PATCH : null, DELETE: typeof DELETE !== 'undefined' ? DELETE : null, options: typeof options !== 'undefined' ? options : null, OPTIONS: typeof OPTIONS !== 'undefined' ? OPTIONS : null, middleware: typeof middleware !== 'undefined' ? middleware : null, generate: typeof generate !== 'undefined' ? generate : null, config: typeof config !== 'undefined' ? config : null, action: typeof action !== 'undefined' ? action : null, revalidate: typeof revalidate !== 'undefined' ? revalidate : null, " + [...actionNames].map((n) => `${n}: typeof ${n} !== 'undefined' ? ${n} : null`).join(", ") + " };";
|
|
54901
55641
|
const injectedKeys = Object.keys(injected);
|
|
54902
55642
|
let module2 = {};
|
|
54903
55643
|
try {
|
|
@@ -54912,6 +55652,8 @@ async function loadTWMModule(filePath, rootDir) {
|
|
|
54912
55652
|
put: H.put || H.PUT || void 0,
|
|
54913
55653
|
delete: H.deleteFn || H.DELETE || void 0,
|
|
54914
55654
|
patch: H.patch || H.PATCH || void 0,
|
|
55655
|
+
// v1.0.8 round 4 (BUG 33): route-level OPTIONS handlers
|
|
55656
|
+
options: H.options || H.OPTIONS || void 0,
|
|
54915
55657
|
middleware: H.middleware || void 0,
|
|
54916
55658
|
generate: H.generate || void 0,
|
|
54917
55659
|
config: H.config || void 0,
|
|
@@ -55047,9 +55789,22 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55047
55789
|
if (rawBody2.length > MAX_BODY_BYTES) {
|
|
55048
55790
|
return { status: 413, json: { ok: false, error: "Payload too large" } };
|
|
55049
55791
|
}
|
|
55050
|
-
|
|
55792
|
+
if (!rawBody2) {
|
|
55793
|
+
parsedBody = {};
|
|
55794
|
+
} else if (__ct.includes("application/x-www-form-urlencoded")) {
|
|
55795
|
+
parsedBody = Object.fromEntries(new URLSearchParams(rawBody2).entries());
|
|
55796
|
+
} else if (__ct.startsWith("text/")) {
|
|
55797
|
+
parsedBody = rawBody2;
|
|
55798
|
+
} else if (__ct.includes("json") || __ct === "") {
|
|
55799
|
+
try {
|
|
55800
|
+
parsedBody = JSON.parse(rawBody2);
|
|
55801
|
+
} catch (e) {
|
|
55802
|
+
return { status: 400, json: { ok: false, error: "Invalid JSON body: " + (e?.message ?? "parse error") } };
|
|
55803
|
+
}
|
|
55804
|
+
} else {
|
|
55805
|
+
return { status: 415, json: { ok: false, error: "Unsupported Content-Type: " + (__ct || "(none)") } };
|
|
55806
|
+
}
|
|
55051
55807
|
} catch {
|
|
55052
|
-
parsedBody = {};
|
|
55053
55808
|
}
|
|
55054
55809
|
} else if (request?.body && typeof request.body === "object") {
|
|
55055
55810
|
parsedBody = request.body;
|
|
@@ -55057,7 +55812,18 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55057
55812
|
let query2 = {};
|
|
55058
55813
|
try {
|
|
55059
55814
|
const u = new URL(request.url);
|
|
55060
|
-
|
|
55815
|
+
for (const [rawKey, value] of u.searchParams.entries()) {
|
|
55816
|
+
const isPhpArray = rawKey.endsWith("[]");
|
|
55817
|
+
const key = isPhpArray ? rawKey.slice(0, -2) : rawKey;
|
|
55818
|
+
const existing = query2[key];
|
|
55819
|
+
if (existing === void 0) {
|
|
55820
|
+
query2[key] = isPhpArray ? [value] : value;
|
|
55821
|
+
} else if (Array.isArray(existing)) {
|
|
55822
|
+
existing.push(value);
|
|
55823
|
+
} else {
|
|
55824
|
+
query2[key] = [existing, value];
|
|
55825
|
+
}
|
|
55826
|
+
}
|
|
55061
55827
|
} catch {
|
|
55062
55828
|
}
|
|
55063
55829
|
const headersObj = typeof request.headers?.entries === "function" ? Object.fromEntries(request.headers.entries()) : request.headers ?? {};
|
|
@@ -55186,9 +55952,17 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55186
55952
|
console.error("[tw] route handler error (" + filePath + "):", err3?.message ?? err3);
|
|
55187
55953
|
return { status: 500, json: { ok: false, error: "Internal Server Error" } };
|
|
55188
55954
|
}
|
|
55189
|
-
if (
|
|
55190
|
-
|
|
55191
|
-
|
|
55955
|
+
if (typeof result === "string") {
|
|
55956
|
+
result = { status: 200, text: result };
|
|
55957
|
+
} else if (!result || typeof result !== "object") {
|
|
55958
|
+
console.error("[tw] route handler returned " + JSON.stringify(result) + " (" + filePath + ")");
|
|
55959
|
+
return {
|
|
55960
|
+
status: 500,
|
|
55961
|
+
json: {
|
|
55962
|
+
ok: false,
|
|
55963
|
+
error: "Unsupported handler return shape: " + (result === null ? "null" : typeof result) + ". Return { status, json|text|html } (or a plain string for text/plain)."
|
|
55964
|
+
}
|
|
55965
|
+
};
|
|
55192
55966
|
}
|
|
55193
55967
|
const finalResult = {
|
|
55194
55968
|
status: typeof result.status === "number" ? result.status : 200,
|