tw-framework 1.0.7 → 1.0.9
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 +1059 -284
- package/dist/tw-server.mjs +56210 -0
- package/dist/tw.mjs +1392 -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.9";
|
|
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' 'unsafe-eval'; 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,11 @@ 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 = // 'unsafe-eval' is needed: the client runtime evaluates state expressions and event handlers with new Function (packages/runtime/tw/client/hydration-runtime.js). Apps that precompile can drop it via the csp option.
|
|
49550
|
+
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; 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
49551
|
DEFAULTS2 = {
|
|
48769
49552
|
hstsMaxAge: 31536e3,
|
|
48770
49553
|
// 1 year
|
|
@@ -48773,7 +49556,9 @@ var init_security_headers = __esm({
|
|
|
48773
49556
|
frameOptions: "SAMEORIGIN",
|
|
48774
49557
|
contentTypeOptions: true,
|
|
48775
49558
|
referrerPolicy: "strict-origin-when-cross-origin",
|
|
48776
|
-
|
|
49559
|
+
// v1.0.8 round 5 (BUG 48): x-xss-protection deprecated since 2019 --
|
|
49560
|
+
// OFF by default now; opt back in with security.xssProtection.
|
|
49561
|
+
xssProtection: void 0,
|
|
48777
49562
|
removePoweredBy: true,
|
|
48778
49563
|
coep: "unsafe-none",
|
|
48779
49564
|
coop: "same-origin",
|
|
@@ -48829,6 +49614,9 @@ var init_security_headers = __esm({
|
|
|
48829
49614
|
if (this.config.serverHeader) {
|
|
48830
49615
|
headers["Server"] = this.config.serverHeader;
|
|
48831
49616
|
}
|
|
49617
|
+
if (this.config.csp !== false && !headers["Content-Security-Policy"]) {
|
|
49618
|
+
headers["Content-Security-Policy"] = typeof this.config.csp === "string" ? this.config.csp : BASELINE_CSP;
|
|
49619
|
+
}
|
|
48832
49620
|
return headers;
|
|
48833
49621
|
}
|
|
48834
49622
|
/** Returns the security headers object. */
|
|
@@ -53039,161 +53827,6 @@ var init_ssr = __esm({
|
|
|
53039
53827
|
}
|
|
53040
53828
|
});
|
|
53041
53829
|
|
|
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
53830
|
// ../../packages/server/tw/edge.ts
|
|
53198
53831
|
function createEdgeHandler(config) {
|
|
53199
53832
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -53912,8 +54545,11 @@ var init_tw6 = __esm({
|
|
|
53912
54545
|
init_routing2();
|
|
53913
54546
|
import_node_path17 = require("node:path");
|
|
53914
54547
|
import_node_fs11 = require("node:fs");
|
|
54548
|
+
init_twm_rules();
|
|
53915
54549
|
init_router();
|
|
53916
54550
|
init_middleware2();
|
|
54551
|
+
init_compression();
|
|
54552
|
+
init_signal_stream();
|
|
53917
54553
|
init_static();
|
|
53918
54554
|
init_tw5();
|
|
53919
54555
|
init_ssr();
|
|
@@ -53962,6 +54598,7 @@ var init_tw6 = __esm({
|
|
|
53962
54598
|
}
|
|
53963
54599
|
ssr = null;
|
|
53964
54600
|
server = null;
|
|
54601
|
+
compressionMode = "off";
|
|
53965
54602
|
isRunning = false;
|
|
53966
54603
|
pluginManager = null;
|
|
53967
54604
|
imageHandler = null;
|
|
@@ -53978,6 +54615,40 @@ var init_tw6 = __esm({
|
|
|
53978
54615
|
const entries = Object.entries(cfgIn.redirects);
|
|
53979
54616
|
cfgIn.redirects = entries.map(([from, to]) => typeof to === "string" ? { from, to } : { from, ...to });
|
|
53980
54617
|
}
|
|
54618
|
+
if (Array.isArray(cfgIn.redirects)) {
|
|
54619
|
+
const REDIRECT_KNOWN = /* @__PURE__ */ new Set(["from", "to", "status", "source", "destination", "permanent"]);
|
|
54620
|
+
let redirectDropped = false;
|
|
54621
|
+
const normalizedRedirects = cfgIn.redirects.map((r) => {
|
|
54622
|
+
if (!r || typeof r !== "object") return r;
|
|
54623
|
+
let touched = false;
|
|
54624
|
+
const mapped = { ...r };
|
|
54625
|
+
if (mapped.from === void 0 && mapped.source !== void 0) {
|
|
54626
|
+
mapped.from = mapped.source;
|
|
54627
|
+
touched = true;
|
|
54628
|
+
}
|
|
54629
|
+
if (mapped.to === void 0 && mapped.destination !== void 0) {
|
|
54630
|
+
mapped.to = mapped.destination;
|
|
54631
|
+
touched = true;
|
|
54632
|
+
}
|
|
54633
|
+
if (mapped.status === void 0 && mapped.permanent !== void 0) {
|
|
54634
|
+
mapped.status = mapped.permanent ? 301 : 302;
|
|
54635
|
+
touched = true;
|
|
54636
|
+
}
|
|
54637
|
+
const unknown = Object.keys(mapped).filter((k) => !REDIRECT_KNOWN.has(k));
|
|
54638
|
+
if (unknown.length > 0) {
|
|
54639
|
+
console.warn("[tw] redirects: unrecognized field" + (unknown.length > 1 ? "s" : "") + " " + unknown.join(", ") + " in entry { from: " + JSON.stringify(mapped.from) + " } -- accepted fields: from, to, status");
|
|
54640
|
+
}
|
|
54641
|
+
if (typeof mapped.from !== "string" || typeof mapped.to !== "string") {
|
|
54642
|
+
console.warn("[tw] redirects: entry missing from/to after normalization -- skipped (" + JSON.stringify(r).slice(0, 80) + ")");
|
|
54643
|
+
redirectDropped = true;
|
|
54644
|
+
return null;
|
|
54645
|
+
}
|
|
54646
|
+
return touched ? mapped : r;
|
|
54647
|
+
}).filter((x) => x !== null || (redirectDropped = true));
|
|
54648
|
+
if (redirectDropped || normalizedRedirects.some((x, i) => x !== cfgIn.redirects[i])) {
|
|
54649
|
+
cfgIn.redirects = normalizedRedirects.filter(Boolean);
|
|
54650
|
+
}
|
|
54651
|
+
}
|
|
53981
54652
|
if (cfgIn.headers && !Array.isArray(cfgIn.headers)) {
|
|
53982
54653
|
const entries = Object.entries(cfgIn.headers);
|
|
53983
54654
|
cfgIn.headers = entries.map(([source, headers]) => ({ source, headers }));
|
|
@@ -53995,6 +54666,12 @@ var init_tw6 = __esm({
|
|
|
53995
54666
|
}
|
|
53996
54667
|
}
|
|
53997
54668
|
this.pluginManager = this.options.plugins ?? null;
|
|
54669
|
+
{
|
|
54670
|
+
const raw = this.options.compression ?? true;
|
|
54671
|
+
if (raw === false || raw === "none" || raw === "off" || raw === "false") this.compressionMode = "off";
|
|
54672
|
+
else if (raw === "gzip") this.compressionMode = "gzip";
|
|
54673
|
+
else this.compressionMode = "brotli";
|
|
54674
|
+
}
|
|
53998
54675
|
try {
|
|
53999
54676
|
this.imageHandler = createImageHandler({
|
|
54000
54677
|
rootDir: this.options.rootDir,
|
|
@@ -54104,31 +54781,72 @@ var init_tw6 = __esm({
|
|
|
54104
54781
|
if ((0, import_node_fs11.existsSync)(middlewarePath)) {
|
|
54105
54782
|
this.middlewarePath = middlewarePath;
|
|
54106
54783
|
console.log(" Middleware: middleware.twm loaded");
|
|
54784
|
+
try {
|
|
54785
|
+
const src = (0, import_node_fs11.readFileSync)(middlewarePath, "utf-8");
|
|
54786
|
+
const rules = parseRules(src, "middleware.twm");
|
|
54787
|
+
for (const r of rules) {
|
|
54788
|
+
const conds = [];
|
|
54789
|
+
if (r.userAgent) conds.push("user_agent");
|
|
54790
|
+
if (r.path) conds.push("path");
|
|
54791
|
+
if (r.auth) conds.push("auth");
|
|
54792
|
+
if (r.rateLimit) conds.push("rate_limit");
|
|
54793
|
+
if (r.origin) conds.push("origin");
|
|
54794
|
+
if (r.methods && r.methods.length > 0) conds.push("methods");
|
|
54795
|
+
const label = conds.length > 0 ? conds.join(" + ") : "no conditions -> every matching request gets this response";
|
|
54796
|
+
console.log(` \u2022 "${r.name}" [${label}]`);
|
|
54797
|
+
}
|
|
54798
|
+
} catch {
|
|
54799
|
+
}
|
|
54107
54800
|
}
|
|
54108
54801
|
return this;
|
|
54109
54802
|
}
|
|
54110
54803
|
async start() {
|
|
54111
54804
|
if (this.isRunning) return;
|
|
54112
|
-
const
|
|
54113
|
-
|
|
54114
|
-
|
|
54115
|
-
|
|
54116
|
-
|
|
54117
|
-
|
|
54118
|
-
|
|
54119
|
-
|
|
54120
|
-
|
|
54121
|
-
|
|
54122
|
-
|
|
54805
|
+
const host = this.options.host ?? "0.0.0.0";
|
|
54806
|
+
const wanted = this.options.port ?? 8e3;
|
|
54807
|
+
let port = wanted;
|
|
54808
|
+
let bound = null;
|
|
54809
|
+
let lastErr = null;
|
|
54810
|
+
for (let p = wanted; p < wanted + 10; p++) {
|
|
54811
|
+
try {
|
|
54812
|
+
bound = (typeof Bun !== "undefined" ? Bun : null)?.serve({
|
|
54813
|
+
port: p,
|
|
54814
|
+
hostname: host,
|
|
54815
|
+
fetch: async (request) => {
|
|
54816
|
+
const res = await this.handleRequest(request);
|
|
54817
|
+
let withHeaders = res;
|
|
54818
|
+
if (this.securityHeaders) {
|
|
54819
|
+
try {
|
|
54820
|
+
withHeaders = this.securityHeaders.apply(res);
|
|
54821
|
+
} catch {
|
|
54822
|
+
withHeaders = res;
|
|
54823
|
+
}
|
|
54824
|
+
}
|
|
54825
|
+
return this.compressResponse(withHeaders, request);
|
|
54826
|
+
},
|
|
54827
|
+
error: (err3) => {
|
|
54828
|
+
console.error("Server error:", err3);
|
|
54829
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
54123
54830
|
}
|
|
54124
|
-
}
|
|
54125
|
-
|
|
54126
|
-
|
|
54127
|
-
|
|
54128
|
-
|
|
54129
|
-
|
|
54831
|
+
});
|
|
54832
|
+
port = p;
|
|
54833
|
+
break;
|
|
54834
|
+
} catch (e) {
|
|
54835
|
+
lastErr = e;
|
|
54836
|
+
const msg = String(e?.message ?? e);
|
|
54837
|
+
if (!/address already in use|EADDRINUSE|Failed to start server/i.test(msg)) throw e;
|
|
54838
|
+
if (p < wanted + 9) console.log(` Port ${p} busy \u2014 trying ${p + 1}...`);
|
|
54130
54839
|
}
|
|
54131
|
-
}
|
|
54840
|
+
}
|
|
54841
|
+
if (!bound) {
|
|
54842
|
+
console.error(`
|
|
54843
|
+
ERROR: could not bind ${host} on ports ${wanted}\u2013${wanted + 9}.`);
|
|
54844
|
+
console.error(` ${lastErr?.message ?? lastErr}
|
|
54845
|
+
`);
|
|
54846
|
+
throw lastErr ?? new Error("could not start server");
|
|
54847
|
+
}
|
|
54848
|
+
this.server = bound;
|
|
54849
|
+
this.options.port = port;
|
|
54132
54850
|
this.isRunning = true;
|
|
54133
54851
|
console.log(`
|
|
54134
54852
|
TW Server running at http://${host}:${port}
|
|
@@ -54142,6 +54860,10 @@ var init_tw6 = __esm({
|
|
|
54142
54860
|
// finishes ongoing requests, then closes
|
|
54143
54861
|
async gracefulShutdown(timeout = 5e3) {
|
|
54144
54862
|
this.isRunning = false;
|
|
54863
|
+
try {
|
|
54864
|
+
getSignalHub().closeAll();
|
|
54865
|
+
} catch {
|
|
54866
|
+
}
|
|
54145
54867
|
if (this.server) {
|
|
54146
54868
|
this.server.stop?.(false);
|
|
54147
54869
|
const start = Date.now();
|
|
@@ -54167,6 +54889,35 @@ var init_tw6 = __esm({
|
|
|
54167
54889
|
* - return { headers: {...} } -> headers merged into the final response
|
|
54168
54890
|
* - return null/undefined -> continue
|
|
54169
54891
|
*/
|
|
54892
|
+
/** BUG 7 (v1.0.8): real response compression. `compression` in
|
|
54893
|
+
* tw.config.ts was silently ignored -- the middleware was a no-op and
|
|
54894
|
+
* nothing on the response path ever set Content-Encoding. */
|
|
54895
|
+
async compressResponse(res, request) {
|
|
54896
|
+
if (this.compressionMode === "off") return res;
|
|
54897
|
+
try {
|
|
54898
|
+
if (res.headers.get("content-encoding")) return res;
|
|
54899
|
+
const ct = res.headers.get("content-type") || "";
|
|
54900
|
+
if (ct.includes("text/event-stream")) return res;
|
|
54901
|
+
if (res.__twRawStream) return res;
|
|
54902
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
54903
|
+
if (!isLargeEnough(buf.length)) return new Response(buf, { status: res.status, headers: res.headers });
|
|
54904
|
+
const accept = request.headers.get("accept-encoding") || "";
|
|
54905
|
+
let enc = negotiateEncoding(accept);
|
|
54906
|
+
if (this.compressionMode === "gzip" && enc === "brotli") enc = "gzip";
|
|
54907
|
+
if (!enc) return new Response(buf, { status: res.status, headers: res.headers });
|
|
54908
|
+
const out = await compress(buf, enc);
|
|
54909
|
+
if (out.length >= buf.length) return new Response(buf, { status: res.status, headers: res.headers });
|
|
54910
|
+
const h = new Headers(res.headers);
|
|
54911
|
+
h.set("Content-Encoding", enc === "brotli" ? "br" : enc);
|
|
54912
|
+
h.set("Vary", "Accept-Encoding");
|
|
54913
|
+
h.delete("Content-Length");
|
|
54914
|
+
return new Response(out, { status: res.status, headers: h });
|
|
54915
|
+
} catch {
|
|
54916
|
+
return res;
|
|
54917
|
+
}
|
|
54918
|
+
}
|
|
54919
|
+
/** v1.0.8 round 5 (BUG 39): public now -- serverless adapters (Vercel
|
|
54920
|
+
* functions, edge runtimes) drive the server through this entry. */
|
|
54170
54921
|
async handleRequest(request) {
|
|
54171
54922
|
try {
|
|
54172
54923
|
const u = new URL(request.url);
|
|
@@ -54339,69 +55090,12 @@ var init_tw6 = __esm({
|
|
|
54339
55090
|
* `data: {"v":1,"seq":N,"updates":[...]}` frames.
|
|
54340
55091
|
*/
|
|
54341
55092
|
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
|
-
});
|
|
55093
|
+
const cfg = this.options.config?.signalStream;
|
|
55094
|
+
return createSignalStream(request, url, cfg ? { maxClients: cfg.maxClients, maxHistory: cfg.maxHistory } : void 0);
|
|
55095
|
+
}
|
|
55096
|
+
async handleClientSignalWrite(request) {
|
|
55097
|
+
const cfg = this.options.config?.signalStream;
|
|
55098
|
+
return writeSignalFromClient(request, { clientWrites: cfg?.clientWrites });
|
|
54405
55099
|
}
|
|
54406
55100
|
/**
|
|
54407
55101
|
* PPR hole fill: render the page fresh and return only the requested
|
|
@@ -54475,15 +55169,33 @@ var init_tw6 = __esm({
|
|
|
54475
55169
|
controller.close();
|
|
54476
55170
|
}
|
|
54477
55171
|
});
|
|
54478
|
-
|
|
55172
|
+
const streamRes = new Response(stream, {
|
|
54479
55173
|
status: 200,
|
|
54480
55174
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
54481
55175
|
});
|
|
55176
|
+
streamRes.__twRawStream = true;
|
|
55177
|
+
return streamRes;
|
|
54482
55178
|
}
|
|
54483
55179
|
async handleRequestInner(request) {
|
|
54484
55180
|
const url = new URL(request.url);
|
|
54485
55181
|
const method = request.method;
|
|
54486
|
-
|
|
55182
|
+
let rawPath = url.pathname;
|
|
55183
|
+
try {
|
|
55184
|
+
const decoded = decodeURIComponent(rawPath);
|
|
55185
|
+
if (decoded !== rawPath) rawPath = decoded;
|
|
55186
|
+
} catch {
|
|
55187
|
+
}
|
|
55188
|
+
let canonical = normalizePathname(rawPath);
|
|
55189
|
+
if (canonical.length > 1 && canonical.endsWith("/")) {
|
|
55190
|
+
canonical = canonical.slice(0, -1);
|
|
55191
|
+
}
|
|
55192
|
+
if (canonical !== rawPath) {
|
|
55193
|
+
return new Response(null, {
|
|
55194
|
+
status: 308,
|
|
55195
|
+
headers: { Location: encodeURI(canonical) + url.search }
|
|
55196
|
+
});
|
|
55197
|
+
}
|
|
55198
|
+
const pathname = canonical;
|
|
54487
55199
|
if (pathname === "/_tw/img" || pathname.startsWith("/_tw/img/")) {
|
|
54488
55200
|
const imgRes = await this.imageHandler.handle(request);
|
|
54489
55201
|
if (imgRes) return imgRes;
|
|
@@ -54491,6 +55203,9 @@ var init_tw6 = __esm({
|
|
|
54491
55203
|
if (pathname === "/_tw/stream") {
|
|
54492
55204
|
return this.handleSignalStream(request, url);
|
|
54493
55205
|
}
|
|
55206
|
+
if (pathname === "/_tw/signal" && method === "POST") {
|
|
55207
|
+
return this.handleClientSignalWrite(request);
|
|
55208
|
+
}
|
|
54494
55209
|
if (pathname === "/_tw/ppr") {
|
|
54495
55210
|
return this.handlePPRHoleFill(url);
|
|
54496
55211
|
}
|
|
@@ -54655,11 +55370,37 @@ __export(twm_loader_exports, {
|
|
|
54655
55370
|
loadTWMModule: () => loadTWMModule,
|
|
54656
55371
|
revalidateRoute: () => revalidateRoute,
|
|
54657
55372
|
shouldMatchMiddleware: () => shouldMatchMiddleware,
|
|
54658
|
-
stripCommentsSafe: () => stripCommentsSafe
|
|
55373
|
+
stripCommentsSafe: () => stripCommentsSafe,
|
|
55374
|
+
validateTWMSyntax: () => validateTWMSyntax
|
|
54659
55375
|
});
|
|
54660
55376
|
function clearTWMCache() {
|
|
54661
55377
|
moduleCache.clear();
|
|
54662
55378
|
}
|
|
55379
|
+
function validateTWMSyntax(filePath) {
|
|
55380
|
+
let source;
|
|
55381
|
+
try {
|
|
55382
|
+
source = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
|
|
55383
|
+
} catch (err3) {
|
|
55384
|
+
return "cannot read: " + (err3?.message ?? err3);
|
|
55385
|
+
}
|
|
55386
|
+
if (isRuleDsl(source)) {
|
|
55387
|
+
try {
|
|
55388
|
+
parseRules(source, filePath);
|
|
55389
|
+
return null;
|
|
55390
|
+
} catch (err3) {
|
|
55391
|
+
return "rule DSL: " + (err3?.message ?? String(err3));
|
|
55392
|
+
}
|
|
55393
|
+
}
|
|
55394
|
+
try {
|
|
55395
|
+
const imports = extractImports(source);
|
|
55396
|
+
const jsCode = compileTwm(source);
|
|
55397
|
+
const codeWithoutImports = stripImports(jsCode);
|
|
55398
|
+
new Function("__tw_unused", codeWithoutImports);
|
|
55399
|
+
return null;
|
|
55400
|
+
} catch (err3) {
|
|
55401
|
+
return (err3?.message ?? String(err3)).replace(/\n/g, " ").slice(0, 200);
|
|
55402
|
+
}
|
|
55403
|
+
}
|
|
54663
55404
|
function stripCommentsSafe(src) {
|
|
54664
55405
|
let out = "";
|
|
54665
55406
|
let i = 0;
|
|
@@ -54897,7 +55638,7 @@ async function loadTWMModule(filePath, rootDir) {
|
|
|
54897
55638
|
console.error("[tw] " + e.message + " (" + filePath + ":" + cm[1] + ")");
|
|
54898
55639
|
}
|
|
54899
55640
|
}
|
|
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(", ") + " };";
|
|
55641
|
+
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
55642
|
const injectedKeys = Object.keys(injected);
|
|
54902
55643
|
let module2 = {};
|
|
54903
55644
|
try {
|
|
@@ -54912,6 +55653,8 @@ async function loadTWMModule(filePath, rootDir) {
|
|
|
54912
55653
|
put: H.put || H.PUT || void 0,
|
|
54913
55654
|
delete: H.deleteFn || H.DELETE || void 0,
|
|
54914
55655
|
patch: H.patch || H.PATCH || void 0,
|
|
55656
|
+
// v1.0.8 round 4 (BUG 33): route-level OPTIONS handlers
|
|
55657
|
+
options: H.options || H.OPTIONS || void 0,
|
|
54915
55658
|
middleware: H.middleware || void 0,
|
|
54916
55659
|
generate: H.generate || void 0,
|
|
54917
55660
|
config: H.config || void 0,
|
|
@@ -55047,9 +55790,22 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55047
55790
|
if (rawBody2.length > MAX_BODY_BYTES) {
|
|
55048
55791
|
return { status: 413, json: { ok: false, error: "Payload too large" } };
|
|
55049
55792
|
}
|
|
55050
|
-
|
|
55793
|
+
if (!rawBody2) {
|
|
55794
|
+
parsedBody = {};
|
|
55795
|
+
} else if (__ct.includes("application/x-www-form-urlencoded")) {
|
|
55796
|
+
parsedBody = Object.fromEntries(new URLSearchParams(rawBody2).entries());
|
|
55797
|
+
} else if (__ct.startsWith("text/")) {
|
|
55798
|
+
parsedBody = rawBody2;
|
|
55799
|
+
} else if (__ct.includes("json") || __ct === "") {
|
|
55800
|
+
try {
|
|
55801
|
+
parsedBody = JSON.parse(rawBody2);
|
|
55802
|
+
} catch (e) {
|
|
55803
|
+
return { status: 400, json: { ok: false, error: "Invalid JSON body: " + (e?.message ?? "parse error") } };
|
|
55804
|
+
}
|
|
55805
|
+
} else {
|
|
55806
|
+
return { status: 415, json: { ok: false, error: "Unsupported Content-Type: " + (__ct || "(none)") } };
|
|
55807
|
+
}
|
|
55051
55808
|
} catch {
|
|
55052
|
-
parsedBody = {};
|
|
55053
55809
|
}
|
|
55054
55810
|
} else if (request?.body && typeof request.body === "object") {
|
|
55055
55811
|
parsedBody = request.body;
|
|
@@ -55057,7 +55813,18 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55057
55813
|
let query2 = {};
|
|
55058
55814
|
try {
|
|
55059
55815
|
const u = new URL(request.url);
|
|
55060
|
-
|
|
55816
|
+
for (const [rawKey, value] of u.searchParams.entries()) {
|
|
55817
|
+
const isPhpArray = rawKey.endsWith("[]");
|
|
55818
|
+
const key = isPhpArray ? rawKey.slice(0, -2) : rawKey;
|
|
55819
|
+
const existing = query2[key];
|
|
55820
|
+
if (existing === void 0) {
|
|
55821
|
+
query2[key] = isPhpArray ? [value] : value;
|
|
55822
|
+
} else if (Array.isArray(existing)) {
|
|
55823
|
+
existing.push(value);
|
|
55824
|
+
} else {
|
|
55825
|
+
query2[key] = [existing, value];
|
|
55826
|
+
}
|
|
55827
|
+
}
|
|
55061
55828
|
} catch {
|
|
55062
55829
|
}
|
|
55063
55830
|
const headersObj = typeof request.headers?.entries === "function" ? Object.fromEntries(request.headers.entries()) : request.headers ?? {};
|
|
@@ -55186,9 +55953,17 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55186
55953
|
console.error("[tw] route handler error (" + filePath + "):", err3?.message ?? err3);
|
|
55187
55954
|
return { status: 500, json: { ok: false, error: "Internal Server Error" } };
|
|
55188
55955
|
}
|
|
55189
|
-
if (
|
|
55190
|
-
|
|
55191
|
-
|
|
55956
|
+
if (typeof result === "string") {
|
|
55957
|
+
result = { status: 200, text: result };
|
|
55958
|
+
} else if (!result || typeof result !== "object") {
|
|
55959
|
+
console.error("[tw] route handler returned " + JSON.stringify(result) + " (" + filePath + ")");
|
|
55960
|
+
return {
|
|
55961
|
+
status: 500,
|
|
55962
|
+
json: {
|
|
55963
|
+
ok: false,
|
|
55964
|
+
error: "Unsupported handler return shape: " + (result === null ? "null" : typeof result) + ". Return { status, json|text|html } (or a plain string for text/plain)."
|
|
55965
|
+
}
|
|
55966
|
+
};
|
|
55192
55967
|
}
|
|
55193
55968
|
const finalResult = {
|
|
55194
55969
|
status: typeof result.status === "number" ? result.status : 200,
|