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/tw.mjs
CHANGED
|
@@ -13372,6 +13372,13 @@ function consumeSignalDeclaration(cursor) {
|
|
|
13372
13372
|
function consumeStateValue(cursor) {
|
|
13373
13373
|
const t2 = cursor.peek();
|
|
13374
13374
|
if (!t2) return "";
|
|
13375
|
+
if (t2.value === "-" || t2.value === "+") {
|
|
13376
|
+
const after = cursor.peek(1);
|
|
13377
|
+
if (after && (after.type === "NUMBER" || after.type === "IDENT" || after.type === "KEYWORD")) {
|
|
13378
|
+
cursor.advance();
|
|
13379
|
+
return (t2.value === "-" ? "-" : "") + String(after.value);
|
|
13380
|
+
}
|
|
13381
|
+
}
|
|
13375
13382
|
if (t2.type === "STRING" || t2.type === "NUMBER" || t2.type === "IDENT" || t2.type === "KEYWORD") {
|
|
13376
13383
|
cursor.advance();
|
|
13377
13384
|
if (t2.type === "STRING") return String(t2.value).replace(/\\([{}"])/g, "$1");
|
|
@@ -14389,6 +14396,20 @@ function parseTWSelector(cursor, errors, state, parseBody) {
|
|
|
14389
14396
|
}
|
|
14390
14397
|
cursor.consumeIf("RBRACE");
|
|
14391
14398
|
} else {
|
|
14399
|
+
for (; ; ) {
|
|
14400
|
+
const t2 = cursor.peek();
|
|
14401
|
+
if (!t2 || t2.type === "RBRACE" || t2.type === "EOF") break;
|
|
14402
|
+
if ((t2.type === "IDENT" || t2.type === "KEYWORD") && ATTR_PAIR_NAMES.has(t2.value)) {
|
|
14403
|
+
const after = cursor.peek(1);
|
|
14404
|
+
if (after && (after.type === "STRING" || after.type === "NUMBER")) {
|
|
14405
|
+
cursor.advance();
|
|
14406
|
+
cursor.advance();
|
|
14407
|
+
el.attrs.push(createAttribute(t2.value, after.value, t2.pos.line, t2.pos.col));
|
|
14408
|
+
continue;
|
|
14409
|
+
}
|
|
14410
|
+
}
|
|
14411
|
+
break;
|
|
14412
|
+
}
|
|
14392
14413
|
el.children = parseBody(["RBRACE"]);
|
|
14393
14414
|
cursor.consumeIf("RBRACE");
|
|
14394
14415
|
}
|
|
@@ -15007,10 +15028,13 @@ function parseWhileStatement(cursor, errors, parseBody) {
|
|
|
15007
15028
|
const whileToken = cursor.advance();
|
|
15008
15029
|
if (!whileToken) return null;
|
|
15009
15030
|
cursor.skipWhitespace();
|
|
15010
|
-
const condToken = cursor.peek();
|
|
15011
15031
|
let condition = "";
|
|
15012
|
-
|
|
15013
|
-
|
|
15032
|
+
while (!cursor.done) {
|
|
15033
|
+
cursor.skipWhitespace();
|
|
15034
|
+
const t2 = cursor.peek();
|
|
15035
|
+
if (!t2 || t2.type === "EOF") break;
|
|
15036
|
+
if (t2.type === "LBRACE" || t2.value === "{") break;
|
|
15037
|
+
condition += (condition ? " " : "") + t2.value;
|
|
15014
15038
|
cursor.advance();
|
|
15015
15039
|
}
|
|
15016
15040
|
const body = parseBlockBody2(cursor, parseBody);
|
|
@@ -15131,13 +15155,133 @@ function parseBlockBody2(cursor, parseBody) {
|
|
|
15131
15155
|
}
|
|
15132
15156
|
return parseBody(["KEYWORD", "RBRACE"]);
|
|
15133
15157
|
}
|
|
15134
|
-
var CLASS_BOUNDARY_BOOLEAN_ATTRS;
|
|
15158
|
+
var ATTR_PAIR_NAMES, CLASS_BOUNDARY_BOOLEAN_ATTRS;
|
|
15135
15159
|
var init_statements = __esm({
|
|
15136
15160
|
"../../packages/compiler/tw/parser/statements/index.ts"() {
|
|
15137
15161
|
init_nodes();
|
|
15138
15162
|
init_recovery();
|
|
15139
15163
|
init_directives();
|
|
15140
15164
|
init_tw();
|
|
15165
|
+
ATTR_PAIR_NAMES = /* @__PURE__ */ new Set([
|
|
15166
|
+
"href",
|
|
15167
|
+
"src",
|
|
15168
|
+
"type",
|
|
15169
|
+
"id",
|
|
15170
|
+
"name",
|
|
15171
|
+
"value",
|
|
15172
|
+
"placeholder",
|
|
15173
|
+
"action",
|
|
15174
|
+
"method",
|
|
15175
|
+
"title",
|
|
15176
|
+
"alt",
|
|
15177
|
+
"rel",
|
|
15178
|
+
"target",
|
|
15179
|
+
"lang",
|
|
15180
|
+
"charset",
|
|
15181
|
+
"content",
|
|
15182
|
+
"width",
|
|
15183
|
+
"height",
|
|
15184
|
+
"loading",
|
|
15185
|
+
"for",
|
|
15186
|
+
"role",
|
|
15187
|
+
"min",
|
|
15188
|
+
"max",
|
|
15189
|
+
"step",
|
|
15190
|
+
"pattern",
|
|
15191
|
+
"accept",
|
|
15192
|
+
"list",
|
|
15193
|
+
"download",
|
|
15194
|
+
"tabindex",
|
|
15195
|
+
"cols",
|
|
15196
|
+
"rows",
|
|
15197
|
+
"wrap",
|
|
15198
|
+
"datetime",
|
|
15199
|
+
"method",
|
|
15200
|
+
"action",
|
|
15201
|
+
"placeholder",
|
|
15202
|
+
"disabled",
|
|
15203
|
+
"required",
|
|
15204
|
+
"readonly",
|
|
15205
|
+
"checked",
|
|
15206
|
+
"selected",
|
|
15207
|
+
"target",
|
|
15208
|
+
"rel",
|
|
15209
|
+
"download",
|
|
15210
|
+
"autocomplete",
|
|
15211
|
+
"autofocus",
|
|
15212
|
+
"min",
|
|
15213
|
+
"max",
|
|
15214
|
+
"step",
|
|
15215
|
+
"pattern",
|
|
15216
|
+
"multiple",
|
|
15217
|
+
"size",
|
|
15218
|
+
"cols",
|
|
15219
|
+
"rows",
|
|
15220
|
+
"wrap",
|
|
15221
|
+
"for",
|
|
15222
|
+
"hidden",
|
|
15223
|
+
"width",
|
|
15224
|
+
"height",
|
|
15225
|
+
"loading",
|
|
15226
|
+
"loop",
|
|
15227
|
+
"muted",
|
|
15228
|
+
"controls",
|
|
15229
|
+
"poster",
|
|
15230
|
+
"preload",
|
|
15231
|
+
"contenteditable",
|
|
15232
|
+
"draggable",
|
|
15233
|
+
"spellcheck",
|
|
15234
|
+
"translate",
|
|
15235
|
+
"accesskey",
|
|
15236
|
+
"tabindex",
|
|
15237
|
+
"role",
|
|
15238
|
+
"slot",
|
|
15239
|
+
"is",
|
|
15240
|
+
"part",
|
|
15241
|
+
"enterkeyhint",
|
|
15242
|
+
"inert",
|
|
15243
|
+
"inputmode",
|
|
15244
|
+
"sandbox",
|
|
15245
|
+
"allow",
|
|
15246
|
+
"allowfullscreen",
|
|
15247
|
+
"referrerpolicy",
|
|
15248
|
+
"fetchpriority",
|
|
15249
|
+
"blocking",
|
|
15250
|
+
"property",
|
|
15251
|
+
"itemprop",
|
|
15252
|
+
"itemtype",
|
|
15253
|
+
"itemid",
|
|
15254
|
+
"itemref",
|
|
15255
|
+
"itemscope",
|
|
15256
|
+
"charset",
|
|
15257
|
+
"http-equiv",
|
|
15258
|
+
"data",
|
|
15259
|
+
"nonce",
|
|
15260
|
+
"crossorigin",
|
|
15261
|
+
"integrity",
|
|
15262
|
+
"async",
|
|
15263
|
+
"defer",
|
|
15264
|
+
"srcset",
|
|
15265
|
+
"sizes",
|
|
15266
|
+
"media",
|
|
15267
|
+
"ping",
|
|
15268
|
+
"coords",
|
|
15269
|
+
"shape",
|
|
15270
|
+
"usemap",
|
|
15271
|
+
"ismap",
|
|
15272
|
+
"kind",
|
|
15273
|
+
"srclang",
|
|
15274
|
+
"default",
|
|
15275
|
+
"autoplay",
|
|
15276
|
+
"playsinline",
|
|
15277
|
+
"enctype",
|
|
15278
|
+
"novalidate",
|
|
15279
|
+
"formaction",
|
|
15280
|
+
"formmethod",
|
|
15281
|
+
"formtarget",
|
|
15282
|
+
"formenctype",
|
|
15283
|
+
"formnovalidate"
|
|
15284
|
+
]);
|
|
15141
15285
|
CLASS_BOUNDARY_BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
|
|
15142
15286
|
"disabled",
|
|
15143
15287
|
"required",
|
|
@@ -22950,7 +23094,7 @@ var init_types4 = __esm({
|
|
|
22950
23094
|
var TW_VERSION, TW_GENERATOR_META;
|
|
22951
23095
|
var init_version = __esm({
|
|
22952
23096
|
"../../packages/compiler/tw/codegen/version.ts"() {
|
|
22953
|
-
TW_VERSION = "1.0.
|
|
23097
|
+
TW_VERSION = "1.0.9";
|
|
22954
23098
|
TW_GENERATOR_META = `<meta name="generator" content="TW Framework ${TW_VERSION}">`;
|
|
22955
23099
|
}
|
|
22956
23100
|
});
|
|
@@ -23337,6 +23481,16 @@ function collectBuiltinImports(program) {
|
|
|
23337
23481
|
for (const dir of program.directives ?? []) {
|
|
23338
23482
|
if (dir.type !== "ImportDirective") continue;
|
|
23339
23483
|
const imp = dir;
|
|
23484
|
+
if (imp.source === "tw" || imp.source === "@tw/server") {
|
|
23485
|
+
for (const item of imp.items ?? []) {
|
|
23486
|
+
const name = item.split(" as ").pop().trim();
|
|
23487
|
+
const spec = TW_NAMED_BUILTINS[name];
|
|
23488
|
+
if (spec) out.set(name, spec);
|
|
23489
|
+
}
|
|
23490
|
+
if (imp.defaultImport && TW_NAMED_BUILTINS[imp.defaultImport]) {
|
|
23491
|
+
out.set(imp.defaultImport, TW_NAMED_BUILTINS[imp.defaultImport]);
|
|
23492
|
+
}
|
|
23493
|
+
}
|
|
23340
23494
|
if (!BUILTIN_COMPONENT_SPECIFIERS.includes(imp.source)) continue;
|
|
23341
23495
|
if (imp.defaultImport) out.set(imp.defaultImport, imp.source);
|
|
23342
23496
|
for (const item of imp.items ?? []) {
|
|
@@ -23406,7 +23560,7 @@ function normalizePrefetchProp(props) {
|
|
|
23406
23560
|
return p;
|
|
23407
23561
|
}
|
|
23408
23562
|
function generateRouterLinkTag(props, textContent) {
|
|
23409
|
-
const href = (props.href ?? "").trim();
|
|
23563
|
+
const href = (props.href ?? props.to ?? "").trim();
|
|
23410
23564
|
if (!href) return `<!-- @tw/RouterLink: missing href -->`;
|
|
23411
23565
|
const opts = {
|
|
23412
23566
|
href,
|
|
@@ -23433,13 +23587,18 @@ function generateBuiltinTag(specifier, props, textContent) {
|
|
|
23433
23587
|
if (specifier === "@tw/RouterLink") return generateRouterLinkTag(props, textContent);
|
|
23434
23588
|
return null;
|
|
23435
23589
|
}
|
|
23436
|
-
var BUILTIN_COMPONENT_SPECIFIERS, imageConfig;
|
|
23590
|
+
var BUILTIN_COMPONENT_SPECIFIERS, imageConfig, TW_NAMED_BUILTINS;
|
|
23437
23591
|
var init_builtin_components = __esm({
|
|
23438
23592
|
"../../packages/compiler/tw/codegen/builtin-components.ts"() {
|
|
23439
23593
|
init_tw2();
|
|
23440
23594
|
init_tw3();
|
|
23441
23595
|
BUILTIN_COMPONENT_SPECIFIERS = ["@tw/optImage", "@tw/RouterLink"];
|
|
23442
23596
|
imageConfig = DEFAULT_IMAGE_CONFIG;
|
|
23597
|
+
TW_NAMED_BUILTINS = {
|
|
23598
|
+
RouterLink: "@tw/RouterLink",
|
|
23599
|
+
optImage: "@tw/optImage",
|
|
23600
|
+
Image: "@tw/optImage"
|
|
23601
|
+
};
|
|
23443
23602
|
}
|
|
23444
23603
|
});
|
|
23445
23604
|
|
|
@@ -24076,6 +24235,58 @@ function stripTssComments(src) {
|
|
|
24076
24235
|
return out;
|
|
24077
24236
|
}
|
|
24078
24237
|
function compileTSS(source) {
|
|
24238
|
+
{
|
|
24239
|
+
let depth = 0;
|
|
24240
|
+
let line = 1;
|
|
24241
|
+
let inString = null;
|
|
24242
|
+
let inComment = false;
|
|
24243
|
+
let inUrl = false;
|
|
24244
|
+
for (let i = 0; i < source.length; i++) {
|
|
24245
|
+
const ch = source[i];
|
|
24246
|
+
const prev = i > 0 ? source[i - 1] : "";
|
|
24247
|
+
if (ch === "\n") {
|
|
24248
|
+
line++;
|
|
24249
|
+
inComment = false;
|
|
24250
|
+
continue;
|
|
24251
|
+
}
|
|
24252
|
+
if (inComment) continue;
|
|
24253
|
+
if (inString) {
|
|
24254
|
+
if (ch === inString && prev !== "\\") inString = null;
|
|
24255
|
+
continue;
|
|
24256
|
+
}
|
|
24257
|
+
if (inUrl) {
|
|
24258
|
+
if (ch === ")") inUrl = false;
|
|
24259
|
+
continue;
|
|
24260
|
+
}
|
|
24261
|
+
if (ch === "/" && source[i + 1] === "/" && source.slice(Math.max(0, i - 3), i).includes(":")) {
|
|
24262
|
+
inUrl = true;
|
|
24263
|
+
continue;
|
|
24264
|
+
}
|
|
24265
|
+
if (ch === "u" && source.slice(i, i + 4) === "url(") {
|
|
24266
|
+
inUrl = true;
|
|
24267
|
+
i += 3;
|
|
24268
|
+
continue;
|
|
24269
|
+
}
|
|
24270
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
24271
|
+
inComment = true;
|
|
24272
|
+
continue;
|
|
24273
|
+
}
|
|
24274
|
+
if (ch === '"' || ch === "'") {
|
|
24275
|
+
inString = ch;
|
|
24276
|
+
continue;
|
|
24277
|
+
}
|
|
24278
|
+
if (ch === "{") depth++;
|
|
24279
|
+
else if (ch === "}") {
|
|
24280
|
+
depth--;
|
|
24281
|
+
if (depth < 0) {
|
|
24282
|
+
throw new Error("TW303: unbalanced '}' in stylesheet (line " + line + ")\n " + (source.split("\n")[line - 1] ?? "").trim().slice(0, 80));
|
|
24283
|
+
}
|
|
24284
|
+
}
|
|
24285
|
+
}
|
|
24286
|
+
if (depth !== 0) {
|
|
24287
|
+
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));
|
|
24288
|
+
}
|
|
24289
|
+
}
|
|
24079
24290
|
const __twOut = __twCompileTSSInner(source);
|
|
24080
24291
|
const leftover = __twOut.match(/\$[A-Za-z_][\w-]*/g);
|
|
24081
24292
|
if (leftover && !/\$\{/.test(__twOut)) {
|
|
@@ -24620,11 +24831,11 @@ function emit(items, parent, env, mixins, out) {
|
|
|
24620
24831
|
}
|
|
24621
24832
|
function compileSCSS(source) {
|
|
24622
24833
|
const cleaned0 = stripComments(source);
|
|
24623
|
-
const UNSUPPORTED = /@(import|
|
|
24834
|
+
const UNSUPPORTED = /@(import|extend|use|forward|function|each|if|else|for|while)\b/g;
|
|
24624
24835
|
const m = cleaned0.match(UNSUPPORTED);
|
|
24625
24836
|
if (m) {
|
|
24626
24837
|
const seen = Array.from(new Set(m.map((x) => x.trim())));
|
|
24627
|
-
console.warn("[scss] unsupported at-rule(s) ignored: " + seen.join(", ") + " -- TW's SCSS subset supports nesting, $vars
|
|
24838
|
+
console.warn("[scss] unsupported at-rule(s) ignored: " + seen.join(", ") + " -- TW's SCSS subset supports nesting, $vars, & references and @mixin/@include");
|
|
24628
24839
|
}
|
|
24629
24840
|
const items = parseItems(stripComments(source));
|
|
24630
24841
|
const out = [];
|
|
@@ -24766,13 +24977,13 @@ function collectTssImports(programs, ctx) {
|
|
|
24766
24977
|
else ctx.inlineStyles.push(css);
|
|
24767
24978
|
}
|
|
24768
24979
|
} catch (e) {
|
|
24769
|
-
if (e && typeof e.message === "string" && /^TW30[
|
|
24980
|
+
if (e && typeof e.message === "string" && /^TW30[123]/.test(e.message)) throw e;
|
|
24770
24981
|
}
|
|
24771
24982
|
}
|
|
24772
24983
|
}
|
|
24773
24984
|
}
|
|
24774
24985
|
} catch (e) {
|
|
24775
|
-
if (e && typeof e.message === "string" && /^TW30[
|
|
24986
|
+
if (e && typeof e.message === "string" && /^TW30[123]/.test(e.message)) throw e;
|
|
24776
24987
|
}
|
|
24777
24988
|
}
|
|
24778
24989
|
function resolveTssPath(fromDir, rel) {
|
|
@@ -24992,6 +25203,10 @@ function generateSuspense(el, ctx) {
|
|
|
24992
25203
|
function generateElement(el, ctx) {
|
|
24993
25204
|
const tag = el.tag;
|
|
24994
25205
|
const voidTag = VOID_TAGS5.has(tag.toLowerCase());
|
|
25206
|
+
if (tag.length > 0 && tag[0] === tag[0].toUpperCase() && !["Link", "Suspense"].includes(tag) && !componentRegistry.has(tag) && !resolveBuiltin(tag, activeBuiltinImports)) {
|
|
25207
|
+
console.warn("[tw] unknown component <" + tag + "> (" + el.line + ":" + el.col + ") -- no import resolves it; nothing was rendered");
|
|
25208
|
+
return "<!-- TW: unknown component '" + tag + "' -- check the import -->";
|
|
25209
|
+
}
|
|
24995
25210
|
{
|
|
24996
25211
|
const builtinSpec = resolveBuiltin(tag, activeBuiltinImports);
|
|
24997
25212
|
if (builtinSpec) {
|
|
@@ -25251,7 +25466,13 @@ function generateComponent(comp, ctx) {
|
|
|
25251
25466
|
ctx.slotContent = savedSlot;
|
|
25252
25467
|
ctx.inHead = compSavedInHead;
|
|
25253
25468
|
} else {
|
|
25254
|
-
|
|
25469
|
+
const known = ["Link", "Suspense"];
|
|
25470
|
+
if (!known.includes(compName)) {
|
|
25471
|
+
console.warn("[tw] unknown component <" + compName + "> (" + comp.line + ":" + comp.col + ") -- unresolvable import; nothing was rendered");
|
|
25472
|
+
childrenHTML = "<!-- TW: unknown component '" + compName + "' -- check the import -->";
|
|
25473
|
+
} else {
|
|
25474
|
+
childrenHTML = comp.children.map((c) => generateNode(c, ctx)).join("");
|
|
25475
|
+
}
|
|
25255
25476
|
}
|
|
25256
25477
|
ctx.stateVars = savedStateVars;
|
|
25257
25478
|
ctx.componentStack.pop();
|
|
@@ -29046,13 +29267,27 @@ ${indentation})` : type;
|
|
|
29046
29267
|
});
|
|
29047
29268
|
|
|
29048
29269
|
// ../../packages/compiler/tw/codegen/vdom.ts
|
|
29270
|
+
function stateLiteralForRender(raw) {
|
|
29271
|
+
const evalNum = (v) => typeof v === "number" || typeof v === "boolean" ? String(v) : null;
|
|
29272
|
+
try {
|
|
29273
|
+
const n = evalNum(JSON.parse(raw));
|
|
29274
|
+
if (n !== null) return n;
|
|
29275
|
+
} catch {
|
|
29276
|
+
}
|
|
29277
|
+
try {
|
|
29278
|
+
const n = evalNum(new Function("return (" + raw + ")")());
|
|
29279
|
+
if (n !== null) return n;
|
|
29280
|
+
} catch {
|
|
29281
|
+
}
|
|
29282
|
+
return raw;
|
|
29283
|
+
}
|
|
29049
29284
|
function generateWithLayout(layoutProgram, pageProgram, stateVars) {
|
|
29050
29285
|
const ctx = createContext("ssr");
|
|
29051
29286
|
if (stateVars) ctx.stateVars = { ...stateVars };
|
|
29052
29287
|
for (const dir of pageProgram.directives || []) {
|
|
29053
29288
|
if (dir.type === "StateDirective") {
|
|
29054
29289
|
for (const decl of dir.declarations || []) {
|
|
29055
|
-
ctx.stateVars[decl.name] = decl.value;
|
|
29290
|
+
ctx.stateVars[decl.name] = stateLiteralForRender(decl.value);
|
|
29056
29291
|
}
|
|
29057
29292
|
}
|
|
29058
29293
|
}
|
|
@@ -29084,7 +29319,7 @@ function generateWithLayoutChain(layoutPrograms, pageProgram, stateVars) {
|
|
|
29084
29319
|
for (const dir of pageProgram.directives || []) {
|
|
29085
29320
|
if (dir.type === "StateDirective") {
|
|
29086
29321
|
for (const decl of dir.declarations || []) {
|
|
29087
|
-
ctx2.stateVars[decl.name] = decl.value;
|
|
29322
|
+
ctx2.stateVars[decl.name] = stateLiteralForRender(decl.value);
|
|
29088
29323
|
}
|
|
29089
29324
|
}
|
|
29090
29325
|
}
|
|
@@ -29275,7 +29510,9 @@ function generate(program, stateVars) {
|
|
|
29275
29510
|
ctx.hasInteractivity = true;
|
|
29276
29511
|
continue;
|
|
29277
29512
|
}
|
|
29278
|
-
|
|
29513
|
+
if (!(decl.name in ctx.stateVars)) {
|
|
29514
|
+
ctx.stateVars[decl.name] = stateLiteralForRender(decl.value);
|
|
29515
|
+
}
|
|
29279
29516
|
let v = decl.value;
|
|
29280
29517
|
try {
|
|
29281
29518
|
v = JSON.parse(decl.value);
|
|
@@ -29608,7 +29845,11 @@ var init_codes = __esm({
|
|
|
29608
29845
|
TW090: { message: "cache { } requires a revalidate window or a life profile", severity: "error", category: "syntax" },
|
|
29609
29846
|
TW091: { message: "fn cached handler is impure (request.cookies/headers/body or setSignal)", severity: "error", category: "semantic" },
|
|
29610
29847
|
TW092: { message: "Unknown cache profile", severity: "error", category: "semantic" },
|
|
29611
|
-
TW093: { message: "Non-deterministic call in a cached handler (value freezes into the cache entry)", severity: "warning", category: "best-practice" }
|
|
29848
|
+
TW093: { message: "Non-deterministic call in a cached handler (value freezes into the cache entry)", severity: "warning", category: "best-practice" },
|
|
29849
|
+
TW094: { message: "Invalid state literal -- array/object elements must be comma-separated (and objects need colons)", severity: "error", category: "syntax" },
|
|
29850
|
+
TW095: { message: "Invalid for-loop iterable -- use {varName} or a valid comma-separated literal", severity: "error", category: "syntax" },
|
|
29851
|
+
TW096: { message: "Unknown state variable in interpolation (renders empty)", severity: "warning", category: "semantic" },
|
|
29852
|
+
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" }
|
|
29612
29853
|
};
|
|
29613
29854
|
}
|
|
29614
29855
|
});
|
|
@@ -29929,6 +30170,124 @@ function checkUnusedDeclarations(program, filePath, diags) {
|
|
|
29929
30170
|
}
|
|
29930
30171
|
}
|
|
29931
30172
|
}
|
|
30173
|
+
function literalIsBroken(value) {
|
|
30174
|
+
const v = value.trim();
|
|
30175
|
+
if (!v.startsWith("[") && !v.startsWith("{")) return false;
|
|
30176
|
+
try {
|
|
30177
|
+
new Function("return (" + v + ")");
|
|
30178
|
+
return false;
|
|
30179
|
+
} catch {
|
|
30180
|
+
return true;
|
|
30181
|
+
}
|
|
30182
|
+
}
|
|
30183
|
+
function checkStateLiterals(program, filePath, diags) {
|
|
30184
|
+
for (const decl of collectStateDecls(program)) {
|
|
30185
|
+
if (decl.signalKind === "derived") continue;
|
|
30186
|
+
if (literalIsBroken(String(decl.value))) {
|
|
30187
|
+
diags.push(createDiagnostic(
|
|
30188
|
+
"TW094",
|
|
30189
|
+
decl.line,
|
|
30190
|
+
decl.col,
|
|
30191
|
+
filePath,
|
|
30192
|
+
`${decl.name} = ${decl.value}`,
|
|
30193
|
+
[
|
|
30194
|
+
`Comma-separate array elements: ${decl.name} = ["a", "b", "c"]`,
|
|
30195
|
+
`Objects need colons: ${decl.name} = { key: "value" }`
|
|
30196
|
+
]
|
|
30197
|
+
));
|
|
30198
|
+
}
|
|
30199
|
+
}
|
|
30200
|
+
}
|
|
30201
|
+
function checkLoopIterables(program, filePath, diags) {
|
|
30202
|
+
forEachNode(program, (node) => {
|
|
30203
|
+
if (!node || node.type !== "For") return;
|
|
30204
|
+
const iterable = String(node.iterable ?? "").trim();
|
|
30205
|
+
if (!iterable) {
|
|
30206
|
+
diags.push(createDiagnostic(
|
|
30207
|
+
"TW095",
|
|
30208
|
+
node.line,
|
|
30209
|
+
node.col,
|
|
30210
|
+
filePath,
|
|
30211
|
+
"for-loop has no iterable",
|
|
30212
|
+
[`Use: for item in {list} { ... }`]
|
|
30213
|
+
));
|
|
30214
|
+
return;
|
|
30215
|
+
}
|
|
30216
|
+
if (/^\{[A-Za-z_][\w.]*\}$/.test(iterable)) return;
|
|
30217
|
+
if (/^[A-Za-z_][\w.]*$/.test(iterable)) return;
|
|
30218
|
+
if (literalIsBroken(iterable)) {
|
|
30219
|
+
diags.push(createDiagnostic(
|
|
30220
|
+
"TW095",
|
|
30221
|
+
node.line,
|
|
30222
|
+
node.col,
|
|
30223
|
+
filePath,
|
|
30224
|
+
"for x in " + iterable,
|
|
30225
|
+
[
|
|
30226
|
+
`Wrap a state variable in braces: for item in {items}`,
|
|
30227
|
+
`Literals need commas: for item in {["a", "b", "c"]}`
|
|
30228
|
+
]
|
|
30229
|
+
));
|
|
30230
|
+
}
|
|
30231
|
+
});
|
|
30232
|
+
}
|
|
30233
|
+
function checkUnknownStateVars(program, filePath, diags) {
|
|
30234
|
+
const known = /* @__PURE__ */ new Set();
|
|
30235
|
+
for (const decl of collectStateDecls(program)) known.add(decl.name);
|
|
30236
|
+
forEachNode(program, (node) => {
|
|
30237
|
+
if (node && node.type === "For" && node.varName) known.add(node.varName);
|
|
30238
|
+
if (node && node.type === "For" && node.indexName) known.add(node.indexName);
|
|
30239
|
+
});
|
|
30240
|
+
const BUILTINS2 = /* @__PURE__ */ new Set([
|
|
30241
|
+
"params",
|
|
30242
|
+
"props",
|
|
30243
|
+
"Math",
|
|
30244
|
+
"Date",
|
|
30245
|
+
"JSON",
|
|
30246
|
+
"String",
|
|
30247
|
+
"Number",
|
|
30248
|
+
"Boolean",
|
|
30249
|
+
"Array",
|
|
30250
|
+
"Object",
|
|
30251
|
+
"window",
|
|
30252
|
+
"true",
|
|
30253
|
+
"false",
|
|
30254
|
+
"null",
|
|
30255
|
+
"undefined"
|
|
30256
|
+
]);
|
|
30257
|
+
forEachNode(program, (node) => {
|
|
30258
|
+
if (!isText(node)) return;
|
|
30259
|
+
const text = String(node.value ?? "");
|
|
30260
|
+
if (!text.includes("{")) return;
|
|
30261
|
+
for (const m of text.matchAll(/\{\s*([A-Za-z_][\w]*)\s*\}/g)) {
|
|
30262
|
+
const name = m[1];
|
|
30263
|
+
if (known.has(name) || BUILTINS2.has(name)) continue;
|
|
30264
|
+
diags.push(createDiagnostic(
|
|
30265
|
+
"TW096",
|
|
30266
|
+
node.line,
|
|
30267
|
+
node.col,
|
|
30268
|
+
filePath,
|
|
30269
|
+
`{${name}}`,
|
|
30270
|
+
[`Declare it in the state block, or check the spelling`]
|
|
30271
|
+
));
|
|
30272
|
+
}
|
|
30273
|
+
});
|
|
30274
|
+
}
|
|
30275
|
+
function checkWhileComparisons(program, filePath, diags) {
|
|
30276
|
+
forEachNode(program, (node) => {
|
|
30277
|
+
if (!node || node.type !== "While") return;
|
|
30278
|
+
const cond = String(node.condition ?? "");
|
|
30279
|
+
if (/[<>=!]/.test(cond)) {
|
|
30280
|
+
diags.push(createDiagnostic(
|
|
30281
|
+
"TW097",
|
|
30282
|
+
node.line,
|
|
30283
|
+
node.col,
|
|
30284
|
+
filePath,
|
|
30285
|
+
"while " + cond,
|
|
30286
|
+
["Use `for item in {list}` for repetition", "while is for boolean flags (state { loading = true })"]
|
|
30287
|
+
));
|
|
30288
|
+
}
|
|
30289
|
+
});
|
|
30290
|
+
}
|
|
29932
30291
|
var HTML_ELEMENTS2, HTML_ATTRIBUTES2, GLOBAL_ATTRIBUTES2, CSS_PROPERTIES2, CSS_ALIASES2, EVENT_TYPES2, ARIA_ROLES2, ARIA_ATTRIBUTES2;
|
|
29933
30292
|
var init_rules = __esm({
|
|
29934
30293
|
"../../packages/compiler/tw/diagnostics/rules.ts"() {
|
|
@@ -30819,6 +31178,10 @@ function diagnose(program, filePath) {
|
|
|
30819
31178
|
checkSemanticErrors(program, filePath, diagnostics);
|
|
30820
31179
|
checkDirectives(program, filePath, diagnostics);
|
|
30821
31180
|
checkUnusedDeclarations(program, filePath, diagnostics);
|
|
31181
|
+
checkStateLiterals(program, filePath, diagnostics);
|
|
31182
|
+
checkLoopIterables(program, filePath, diagnostics);
|
|
31183
|
+
checkUnknownStateVars(program, filePath, diagnostics);
|
|
31184
|
+
checkWhileComparisons(program, filePath, diagnostics);
|
|
30822
31185
|
return diagnostics;
|
|
30823
31186
|
}
|
|
30824
31187
|
var RuleEngine;
|
|
@@ -40443,6 +40806,8 @@ function matchPath(pattern, pathname) {
|
|
|
40443
40806
|
}
|
|
40444
40807
|
function ruleBlocked(rule, request, pathname) {
|
|
40445
40808
|
const headers = request.headers;
|
|
40809
|
+
const hasCondition = rule.userAgent || rule.path || rule.auth || rule.rateLimit || rule.origin || rule.methods && rule.methods.length > 0;
|
|
40810
|
+
if (!hasCondition) return true;
|
|
40446
40811
|
if (rule.userAgent) {
|
|
40447
40812
|
const ua = (headers.get("user-agent") ?? "").toLowerCase();
|
|
40448
40813
|
if (!ua) {
|
|
@@ -41011,6 +41376,15 @@ var init_revalidate = __esm({
|
|
|
41011
41376
|
});
|
|
41012
41377
|
|
|
41013
41378
|
// ../../packages/server/tw/routing/signal-stream.ts
|
|
41379
|
+
var signal_stream_exports = {};
|
|
41380
|
+
__export(signal_stream_exports, {
|
|
41381
|
+
SignalHub: () => SignalHub,
|
|
41382
|
+
createSignalStream: () => createSignalStream,
|
|
41383
|
+
getSignalHub: () => getSignalHub,
|
|
41384
|
+
setSignal: () => setSignal,
|
|
41385
|
+
sseFrameWithId: () => sseFrameWithId,
|
|
41386
|
+
writeSignalFromClient: () => writeSignalFromClient
|
|
41387
|
+
});
|
|
41014
41388
|
function getSignalHub() {
|
|
41015
41389
|
if (!hub) hub = new SignalHub();
|
|
41016
41390
|
return hub;
|
|
@@ -41032,11 +41406,158 @@ function setSignal(name, value, opts) {
|
|
|
41032
41406
|
}
|
|
41033
41407
|
return getSignalHub().setSignal(name, value, session ? { session } : void 0);
|
|
41034
41408
|
}
|
|
41035
|
-
|
|
41409
|
+
function sseFrameWithId(payload) {
|
|
41410
|
+
try {
|
|
41411
|
+
const seq = JSON.parse(payload).seq;
|
|
41412
|
+
if (typeof seq === "number") return "id: " + seq + "\ndata: " + payload + "\n\n";
|
|
41413
|
+
} catch {
|
|
41414
|
+
}
|
|
41415
|
+
return "data: " + payload + "\n\n";
|
|
41416
|
+
}
|
|
41417
|
+
function createSignalStream(request, url, opts) {
|
|
41418
|
+
const hub2 = getSignalHub();
|
|
41419
|
+
if (opts && !hub2.__cfg) {
|
|
41420
|
+
hub2.__cfg = true;
|
|
41421
|
+
try {
|
|
41422
|
+
hub2.configure({ maxClients: opts.maxClients, maxHistory: opts.maxHistory });
|
|
41423
|
+
} catch {
|
|
41424
|
+
}
|
|
41425
|
+
}
|
|
41426
|
+
if (url.searchParams.get("stats")) {
|
|
41427
|
+
return new Response(JSON.stringify(hub2.getStats(), null, 2), {
|
|
41428
|
+
status: 200,
|
|
41429
|
+
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }
|
|
41430
|
+
});
|
|
41431
|
+
}
|
|
41432
|
+
if (!hub2.canAcceptClient()) {
|
|
41433
|
+
return new Response("Too many signal-stream connections", {
|
|
41434
|
+
status: 503,
|
|
41435
|
+
headers: { "Content-Type": "text/plain", "Retry-After": "5" }
|
|
41436
|
+
});
|
|
41437
|
+
}
|
|
41438
|
+
const names = (url.searchParams.get("s") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
41439
|
+
let since = Number(url.searchParams.get("since") ?? "0") || 0;
|
|
41440
|
+
if (!since) {
|
|
41441
|
+
const lei = Number(request.headers.get("last-event-id") ?? "0") || 0;
|
|
41442
|
+
if (lei) since = lei;
|
|
41443
|
+
}
|
|
41444
|
+
const declared = /* @__PURE__ */ new Map();
|
|
41445
|
+
for (const n of names) declared.set(n, "public");
|
|
41446
|
+
let session;
|
|
41447
|
+
try {
|
|
41448
|
+
const header = request.headers.get("cookie") ?? "";
|
|
41449
|
+
const m = /(?:^|;\s*)tw_session=([^;]+)/.exec(header);
|
|
41450
|
+
if (m) session = decodeURIComponent(m[1].trim().replace(/^"|"$/g, ""));
|
|
41451
|
+
} catch {
|
|
41452
|
+
}
|
|
41453
|
+
const enc = new TextEncoder();
|
|
41454
|
+
const sse = (payload) => enc.encode(sseFrameWithId(payload));
|
|
41455
|
+
let registered = null;
|
|
41456
|
+
let keepAlive = null;
|
|
41457
|
+
const stream = new ReadableStream({
|
|
41458
|
+
start(controller) {
|
|
41459
|
+
const client = hub2.register(declared, session);
|
|
41460
|
+
registered = client;
|
|
41461
|
+
const cleanup = () => {
|
|
41462
|
+
if (keepAlive) clearInterval(keepAlive);
|
|
41463
|
+
hub2.unregister(client.id);
|
|
41464
|
+
try {
|
|
41465
|
+
controller.close();
|
|
41466
|
+
} catch {
|
|
41467
|
+
}
|
|
41468
|
+
};
|
|
41469
|
+
client.send = (payload) => {
|
|
41470
|
+
try {
|
|
41471
|
+
controller.enqueue(sse(payload));
|
|
41472
|
+
} catch {
|
|
41473
|
+
cleanup();
|
|
41474
|
+
}
|
|
41475
|
+
};
|
|
41476
|
+
client.close = cleanup;
|
|
41477
|
+
for (const payload of hub2.connectPayloads(since, declared, session)) {
|
|
41478
|
+
try {
|
|
41479
|
+
controller.enqueue(sse(payload));
|
|
41480
|
+
} catch {
|
|
41481
|
+
break;
|
|
41482
|
+
}
|
|
41483
|
+
}
|
|
41484
|
+
keepAlive = setInterval(() => {
|
|
41485
|
+
try {
|
|
41486
|
+
controller.enqueue(enc.encode(": keep-alive\n\n"));
|
|
41487
|
+
} catch {
|
|
41488
|
+
cleanup();
|
|
41489
|
+
}
|
|
41490
|
+
}, 25e3);
|
|
41491
|
+
},
|
|
41492
|
+
// v2: when the browser drops the connection (tab close, network
|
|
41493
|
+
// loss) cancel fires -- remove THIS hub entry immediately instead
|
|
41494
|
+
// of waiting for the next failed send.
|
|
41495
|
+
cancel() {
|
|
41496
|
+
if (keepAlive) clearInterval(keepAlive);
|
|
41497
|
+
try {
|
|
41498
|
+
registered?.close?.();
|
|
41499
|
+
} catch {
|
|
41500
|
+
}
|
|
41501
|
+
}
|
|
41502
|
+
});
|
|
41503
|
+
return new Response(stream, {
|
|
41504
|
+
status: 200,
|
|
41505
|
+
headers: {
|
|
41506
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
41507
|
+
"Cache-Control": "no-store",
|
|
41508
|
+
"X-Accel-Buffering": "no",
|
|
41509
|
+
Connection: "keep-alive"
|
|
41510
|
+
}
|
|
41511
|
+
});
|
|
41512
|
+
}
|
|
41513
|
+
async function writeSignalFromClient(request, opts) {
|
|
41514
|
+
if (!opts?.clientWrites) {
|
|
41515
|
+
return new Response(JSON.stringify({ error: "client signal writes are disabled" }), {
|
|
41516
|
+
status: 403,
|
|
41517
|
+
headers: { "Content-Type": "application/json" }
|
|
41518
|
+
});
|
|
41519
|
+
}
|
|
41520
|
+
let body = null;
|
|
41521
|
+
try {
|
|
41522
|
+
body = await request.json();
|
|
41523
|
+
} catch {
|
|
41524
|
+
}
|
|
41525
|
+
const name = body?.name;
|
|
41526
|
+
if (typeof name !== "string" || !("value" in (body ?? {}))) {
|
|
41527
|
+
return new Response(JSON.stringify({ error: "body must be { name, value }" }), {
|
|
41528
|
+
status: 400,
|
|
41529
|
+
headers: { "Content-Type": "application/json" }
|
|
41530
|
+
});
|
|
41531
|
+
}
|
|
41532
|
+
const hub2 = getSignalHub();
|
|
41533
|
+
const kind = (hub2.getStats().signals ?? {})[name];
|
|
41534
|
+
if (kind && kind !== "public") {
|
|
41535
|
+
return new Response(JSON.stringify({ error: "only public signals are client-writable" }), {
|
|
41536
|
+
status: 403,
|
|
41537
|
+
headers: { "Content-Type": "application/json" }
|
|
41538
|
+
});
|
|
41539
|
+
}
|
|
41540
|
+
const res = hub2.setSignal(name, body.value);
|
|
41541
|
+
if (!res.queued) {
|
|
41542
|
+
return new Response(JSON.stringify({ error: res.error ?? "rejected" }), {
|
|
41543
|
+
status: 400,
|
|
41544
|
+
headers: { "Content-Type": "application/json" }
|
|
41545
|
+
});
|
|
41546
|
+
}
|
|
41547
|
+
return new Response(JSON.stringify({ ok: true, kind: res.kind }), {
|
|
41548
|
+
status: 202,
|
|
41549
|
+
headers: { "Content-Type": "application/json" }
|
|
41550
|
+
});
|
|
41551
|
+
}
|
|
41552
|
+
var BATCH_MS, HISTORY_LIMIT, MAX_CLIENTS, MAX_VALUE_BYTES, MAX_NAME_LEN, NAME_RE, SignalHub, hub;
|
|
41036
41553
|
var init_signal_stream = __esm({
|
|
41037
41554
|
"../../packages/server/tw/routing/signal-stream.ts"() {
|
|
41038
41555
|
BATCH_MS = 100;
|
|
41039
41556
|
HISTORY_LIMIT = 1e3;
|
|
41557
|
+
MAX_CLIENTS = 500;
|
|
41558
|
+
MAX_VALUE_BYTES = 64 * 1024;
|
|
41559
|
+
MAX_NAME_LEN = 64;
|
|
41560
|
+
NAME_RE = /^[A-Za-z0-9_$.-]{1,64}$/;
|
|
41040
41561
|
SignalHub = class {
|
|
41041
41562
|
clients = /* @__PURE__ */ new Map();
|
|
41042
41563
|
nextClientId = 1;
|
|
@@ -41047,12 +41568,82 @@ var init_signal_stream = __esm({
|
|
|
41047
41568
|
pending = /* @__PURE__ */ new Map();
|
|
41048
41569
|
pendingSession;
|
|
41049
41570
|
timer = null;
|
|
41571
|
+
// v2 stats + caps
|
|
41572
|
+
maxClients = MAX_CLIENTS;
|
|
41573
|
+
createdAt = Date.now();
|
|
41574
|
+
totalUpdates = 0;
|
|
41575
|
+
batchesFlushed = 0;
|
|
41576
|
+
rejectedCount = 0;
|
|
41577
|
+
/** v2: configure caps (from tw.config.ts signalStream options). */
|
|
41578
|
+
configure(opts) {
|
|
41579
|
+
if (opts?.maxClients && opts.maxClients > 0) this.maxClients = opts.maxClients;
|
|
41580
|
+
if (opts?.maxHistory && opts.maxHistory > 0) this.historyLimit = opts.maxHistory;
|
|
41581
|
+
}
|
|
41582
|
+
historyLimit = HISTORY_LIMIT;
|
|
41583
|
+
/** v2: observability for /_tw/stream?stats=1 and window.__tw stats. */
|
|
41584
|
+
getStats() {
|
|
41585
|
+
const signals = {};
|
|
41586
|
+
for (const [n, k] of this.kinds) signals[n] = k;
|
|
41587
|
+
return {
|
|
41588
|
+
clients: this.clients.size,
|
|
41589
|
+
clientCap: this.maxClients,
|
|
41590
|
+
seq: this.seq,
|
|
41591
|
+
signals,
|
|
41592
|
+
valueCount: this.values.size,
|
|
41593
|
+
history: this.history.length,
|
|
41594
|
+
historyCap: this.historyLimit,
|
|
41595
|
+
pending: this.pending.size,
|
|
41596
|
+
totalUpdates: this.totalUpdates,
|
|
41597
|
+
batchesFlushed: this.batchesFlushed,
|
|
41598
|
+
rejected: this.rejectedCount,
|
|
41599
|
+
uptimeMs: Date.now() - this.createdAt
|
|
41600
|
+
};
|
|
41601
|
+
}
|
|
41602
|
+
/** v2: graceful shutdown -- close every connected stream now. */
|
|
41603
|
+
closeAll() {
|
|
41604
|
+
for (const c of [...this.clients.values()]) {
|
|
41605
|
+
try {
|
|
41606
|
+
c.close();
|
|
41607
|
+
} catch {
|
|
41608
|
+
}
|
|
41609
|
+
}
|
|
41610
|
+
this.clients.clear();
|
|
41611
|
+
if (this.timer != null) {
|
|
41612
|
+
clearTimeout(this.timer);
|
|
41613
|
+
this.timer = null;
|
|
41614
|
+
}
|
|
41615
|
+
this.pending.clear();
|
|
41616
|
+
}
|
|
41617
|
+
/** v2: connection cap check BEFORE registering a stream client. */
|
|
41618
|
+
canAcceptClient() {
|
|
41619
|
+
return this.clients.size < this.maxClients;
|
|
41620
|
+
}
|
|
41621
|
+
/** v2: validate a setSignal name/value without throwing. */
|
|
41622
|
+
validateSignal(name, value) {
|
|
41623
|
+
if (typeof name !== "string" || name.length === 0 || name.length > MAX_NAME_LEN) {
|
|
41624
|
+
return "signal name must be 1-" + MAX_NAME_LEN + " chars";
|
|
41625
|
+
}
|
|
41626
|
+
if (!NAME_RE.test(name)) return "signal name has invalid characters";
|
|
41627
|
+
if (value === void 0) return "signal value is undefined";
|
|
41628
|
+
try {
|
|
41629
|
+
const size = typeof value === "string" ? value.length : JSON.stringify(value)?.length ?? 0;
|
|
41630
|
+
if (size > MAX_VALUE_BYTES) return "signal value exceeds " + MAX_VALUE_BYTES + " bytes";
|
|
41631
|
+
} catch {
|
|
41632
|
+
return "signal value is not serializable";
|
|
41633
|
+
}
|
|
41634
|
+
return null;
|
|
41635
|
+
}
|
|
41050
41636
|
/**
|
|
41051
41637
|
* Server-side mutation (route handlers): queue an update, batch-flush.
|
|
41052
41638
|
* With `session` (derived from the request's tw_session cookie) private
|
|
41053
41639
|
* signals are delivered only to clients authenticated as that session.
|
|
41054
41640
|
*/
|
|
41055
41641
|
setSignal(name, value, opts) {
|
|
41642
|
+
const invalid = this.validateSignal(name, value);
|
|
41643
|
+
if (invalid) {
|
|
41644
|
+
this.rejectedCount++;
|
|
41645
|
+
return { queued: false, kind: this.kinds.get(name), error: invalid };
|
|
41646
|
+
}
|
|
41056
41647
|
const kind = this.kinds.get(name) ?? "public";
|
|
41057
41648
|
this.values.set(name, value);
|
|
41058
41649
|
this.kinds.set(name, kind);
|
|
@@ -41083,9 +41674,27 @@ var init_signal_stream = __esm({
|
|
|
41083
41674
|
this.clients.set(id, client);
|
|
41084
41675
|
return client;
|
|
41085
41676
|
}
|
|
41677
|
+
/** v2: remove a stream client (its connection died / was cancelled).
|
|
41678
|
+
* The old handler overwrote client.close() with a closure that only
|
|
41679
|
+
* closed the controller -- the Map entry leaked forever, so every
|
|
41680
|
+
* disconnected browser still occupied a slot (and got flushed frames).
|
|
41681
|
+
*/
|
|
41682
|
+
unregister(id) {
|
|
41683
|
+
this.clients.delete(id);
|
|
41684
|
+
}
|
|
41086
41685
|
/** Build the replay/snapshot preamble for a connecting client. */
|
|
41087
41686
|
connectPayloads(since, declared, session) {
|
|
41088
41687
|
const out = [];
|
|
41688
|
+
if (since <= 0 && this.values.size > 0) {
|
|
41689
|
+
const snap = {};
|
|
41690
|
+
for (const [name, value] of this.values) {
|
|
41691
|
+
if (this.sessionMatches(session, { names: [name], session: this.valueSessions.get(name) }) && this.deliversTo(declared, [name])) snap[name] = value;
|
|
41692
|
+
}
|
|
41693
|
+
if (Object.keys(snap).length > 0) {
|
|
41694
|
+
out.push(this.frame({ v: 1, seq: this.seq, snapshot: snap }));
|
|
41695
|
+
}
|
|
41696
|
+
return out;
|
|
41697
|
+
}
|
|
41089
41698
|
const hasCoverage = since <= this.seq && this.history.length > 0 && since >= this.history[0].seq - 1;
|
|
41090
41699
|
if (hasCoverage) {
|
|
41091
41700
|
for (const entry of this.history) {
|
|
@@ -41128,7 +41737,9 @@ var init_signal_stream = __esm({
|
|
|
41128
41737
|
for (const [n] of updates) this.valueSessions.set(n, session);
|
|
41129
41738
|
const payload = this.frame({ v: 1, seq: this.seq, updates });
|
|
41130
41739
|
this.history.push({ seq: this.seq, payload, names, session });
|
|
41131
|
-
|
|
41740
|
+
this.totalUpdates += updates.length;
|
|
41741
|
+
this.batchesFlushed++;
|
|
41742
|
+
if (this.history.length > this.historyLimit) this.history.shift();
|
|
41132
41743
|
for (const client of this.clients.values()) {
|
|
41133
41744
|
if (this.sessionMatches(client.session, { names, session }) && this.deliversTo(client.declared, names)) client.send(payload);
|
|
41134
41745
|
}
|
|
@@ -41815,6 +42426,7 @@ var init_render_pipeline = __esm({
|
|
|
41815
42426
|
init_twm_loader();
|
|
41816
42427
|
init_scanner();
|
|
41817
42428
|
init_layout_chain();
|
|
42429
|
+
init_signal_stream();
|
|
41818
42430
|
init_helpers();
|
|
41819
42431
|
init_tw6();
|
|
41820
42432
|
init_twm_loader();
|
|
@@ -41968,6 +42580,17 @@ var init_render_pipeline = __esm({
|
|
|
41968
42580
|
try {
|
|
41969
42581
|
const pageState = { ...stateVars ?? {}, ...match.params ?? {}, params: match.params ?? {} };
|
|
41970
42582
|
const requestVarKeys = new Set(Object.keys({ ...stateVars ?? {}, ...match.params ?? {} }));
|
|
42583
|
+
try {
|
|
42584
|
+
const hub2 = getSignalHub();
|
|
42585
|
+
const snap = hub2.snapshot();
|
|
42586
|
+
if (snap && snap.snapshot && typeof snap.snapshot === "object") {
|
|
42587
|
+
for (const [k, v] of Object.entries(snap.snapshot)) {
|
|
42588
|
+
pageState[k] = v;
|
|
42589
|
+
requestVarKeys.add(k);
|
|
42590
|
+
}
|
|
42591
|
+
}
|
|
42592
|
+
} catch {
|
|
42593
|
+
}
|
|
41971
42594
|
const pageCompiled = this.compileFile(ctx.page, pageState);
|
|
41972
42595
|
{
|
|
41973
42596
|
const seed = pageCompiled.stateSeed;
|
|
@@ -42013,24 +42636,30 @@ var init_render_pipeline = __esm({
|
|
|
42013
42636
|
let layoutTitle;
|
|
42014
42637
|
const liftedParts = [];
|
|
42015
42638
|
{
|
|
42016
|
-
|
|
42639
|
+
const collectedTitles = [];
|
|
42640
|
+
for (const headMatch of bodyHTML.matchAll(/<head(?:\s[^>]*)?>([\s\S]*?)<\/head\s*>/gi)) {
|
|
42017
42641
|
const headInner = headMatch[1];
|
|
42018
42642
|
const tMatch = headInner.match(/<title>([^<]*)<\/title>/i);
|
|
42019
|
-
if (tMatch)
|
|
42020
|
-
|
|
42643
|
+
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)));
|
|
42644
|
+
else collectedTitles.push("");
|
|
42645
|
+
const kids = headInner.replace(/<title>[^<]*<\/title>/gi, "").replace(/<meta[^>]+charset[^>]*>/gi, "").replace(/<meta[^>]+viewport[^>]*>/gi, "").replace(/<meta[^>]+generator[^>]*>/gi, "").trim();
|
|
42021
42646
|
if (kids) liftedParts.push(kids);
|
|
42022
42647
|
}
|
|
42023
|
-
bodyHTML = bodyHTML.replace(/<head[^>]
|
|
42648
|
+
bodyHTML = bodyHTML.replace(/<head(?:\s[^>]*)?>[\s\S]*?<\/head\s*>/gi, "");
|
|
42649
|
+
const templateTitle = collectedTitles.find((t2) => t2.includes("__TW_PAGETITLE__"));
|
|
42650
|
+
layoutTitle = templateTitle ?? collectedTitles.filter(Boolean).pop();
|
|
42024
42651
|
bodyHTML = bodyHTML.replace(/<\/?(?:html|body)[^>]*>/gi, "");
|
|
42025
42652
|
bodyHTML = bodyHTML.replace(/<!DOCTYPE[^>]*>/gi, "");
|
|
42026
42653
|
}
|
|
42027
42654
|
const liftedHead = liftedParts.join("\n");
|
|
42028
|
-
|
|
42655
|
+
const liveRenderMode = this.extractRenderMode(ctx.page);
|
|
42656
|
+
if (ctx.error && (liveRenderMode === "ssr" || liveRenderMode === "stream")) {
|
|
42029
42657
|
const errorCompiled = this.compileFile(ctx.error);
|
|
42030
42658
|
allCSS = errorCompiled.css + "\n" + allCSS;
|
|
42031
42659
|
bodyHTML = `<div data-error-boundary>${bodyHTML}</div>`;
|
|
42032
42660
|
}
|
|
42033
|
-
|
|
42661
|
+
const needsSkeleton = bodyHTML.includes("data-tw-suspense") || bodyHTML.includes("data-tw-ppr");
|
|
42662
|
+
if (ctx.loading && needsSkeleton) {
|
|
42034
42663
|
const loadingCompiled = this.compileFile(ctx.loading);
|
|
42035
42664
|
allCSS = loadingCompiled.css + "\n" + allCSS;
|
|
42036
42665
|
let lh = String(loadingCompiled.html || "");
|
|
@@ -42042,7 +42671,7 @@ ${bodyHTML}`;
|
|
|
42042
42671
|
const pageTitle = this.extractTitle(ctx.page) || "TW Page";
|
|
42043
42672
|
let title = pageTitle;
|
|
42044
42673
|
if (layoutTitle !== void 0) {
|
|
42045
|
-
title = layoutTitle.replace(/\{\s*page\.title\s*\}/g, pageTitle);
|
|
42674
|
+
title = layoutTitle.replace(/__TW_PAGETITLE__/g, pageTitle).replace(/\{\s*page\.title\s*\}/g, pageTitle);
|
|
42046
42675
|
if (!title.trim()) title = pageTitle;
|
|
42047
42676
|
}
|
|
42048
42677
|
const metaTags = this.buildMetaTags(ctx.page);
|
|
@@ -42062,7 +42691,9 @@ ${bodyHTML}`;
|
|
|
42062
42691
|
} catch {
|
|
42063
42692
|
}
|
|
42064
42693
|
let fullHTML = this.assembleHTML(bodyHTML, allCSS, allJS, title, metaTags + headExtra);
|
|
42065
|
-
|
|
42694
|
+
const pageRenderMode = this.extractRenderMode(ctx.page);
|
|
42695
|
+
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";
|
|
42696
|
+
if (needsClientRuntime && !/__tw_runtime\.js/.test(fullHTML)) {
|
|
42066
42697
|
let stateSeed = "{}";
|
|
42067
42698
|
try {
|
|
42068
42699
|
stateSeed = JSON.stringify(pageState ?? {});
|
|
@@ -42181,7 +42812,8 @@ ${bodyHTML}`;
|
|
|
42181
42812
|
throw new Error(`Route file not found: ${file.absolutePath}`);
|
|
42182
42813
|
}
|
|
42183
42814
|
const source = readFileSync7(file.absolutePath, "utf-8");
|
|
42184
|
-
const
|
|
42815
|
+
const srcWithSentinel = source.replace(/\{\s*page\.title\s*\}/g, "__TW_PAGETITLE__");
|
|
42816
|
+
const result = compileSync(srcWithSentinel, {
|
|
42185
42817
|
filePath: file.absolutePath,
|
|
42186
42818
|
optimize: true,
|
|
42187
42819
|
diagnostics: false,
|
|
@@ -42255,12 +42887,13 @@ ${bodyHTML}`;
|
|
|
42255
42887
|
try {
|
|
42256
42888
|
const source = readFileSync7(file.absolutePath, "utf-8");
|
|
42257
42889
|
const masked = maskSourceStringsAndComments(source);
|
|
42258
|
-
const m = masked.match(/page\s*\{[^}]*title\s
|
|
42890
|
+
const m = masked.match(/page\s*\{[^}]*title\s/);
|
|
42259
42891
|
if (!m || m.index === void 0) return null;
|
|
42260
42892
|
const start = m.index + m[0].length;
|
|
42261
|
-
|
|
42262
|
-
|
|
42263
|
-
|
|
42893
|
+
if (source[start] !== '"') return null;
|
|
42894
|
+
const end = source.indexOf('"', start + 1);
|
|
42895
|
+
if (end === -1 || end === start + 1) return null;
|
|
42896
|
+
return source.slice(start + 1, end).replace(/\\([{}"])/g, "$1");
|
|
42264
42897
|
} catch {
|
|
42265
42898
|
return null;
|
|
42266
42899
|
}
|
|
@@ -42309,7 +42942,8 @@ ${bodyHTML}`;
|
|
|
42309
42942
|
* render-cache never reads `stale`.
|
|
42310
42943
|
*/
|
|
42311
42944
|
stampCacheHeaders(headers, entry, now, verdict) {
|
|
42312
|
-
const out = { ...headers ?? {}
|
|
42945
|
+
const out = { ...headers ?? {} };
|
|
42946
|
+
if (process.env.TW_DEBUG_CACHE || process.env.NODE_ENV === "test") out["x-tw-cache"] = verdict;
|
|
42313
42947
|
const c = entry?.cache;
|
|
42314
42948
|
if (c) {
|
|
42315
42949
|
const ageSec = entry?.createdAt ? Math.max(0, Math.floor((now - entry.createdAt) / 1e3)) : 0;
|
|
@@ -43843,7 +44477,6 @@ function securityHeadersMiddleware() {
|
|
|
43843
44477
|
return (req, res, next) => {
|
|
43844
44478
|
res.headers.set("X-Content-Type-Options", "nosniff");
|
|
43845
44479
|
res.headers.set("X-Frame-Options", "SAMEORIGIN");
|
|
43846
|
-
res.headers.set("X-XSS-Protection", "1; mode=block");
|
|
43847
44480
|
res.headers.set("X-Permitted-Cross-Domain-Policies", "none");
|
|
43848
44481
|
res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
43849
44482
|
res.headers.set("Cross-Origin-Opener-Policy", "same-origin");
|
|
@@ -44020,13 +44653,15 @@ function compressionMiddleware() {
|
|
|
44020
44653
|
await next();
|
|
44021
44654
|
};
|
|
44022
44655
|
}
|
|
44023
|
-
function securityHeadersMiddleware2() {
|
|
44656
|
+
function securityHeadersMiddleware2(opts) {
|
|
44024
44657
|
return async (ctx, next) => {
|
|
44025
44658
|
ctx.headers["x-content-type-options"] = "nosniff";
|
|
44026
44659
|
ctx.headers["x-frame-options"] = "DENY";
|
|
44027
|
-
ctx.headers["x-xss-protection"] = "1; mode=block";
|
|
44028
44660
|
ctx.headers["referrer-policy"] = "strict-origin-when-cross-origin";
|
|
44029
44661
|
ctx.headers["permissions-policy"] = "camera=(), microphone=(), geolocation=()";
|
|
44662
|
+
if (opts?.csp !== false && !ctx.headers["content-security-policy"]) {
|
|
44663
|
+
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'";
|
|
44664
|
+
}
|
|
44030
44665
|
await next();
|
|
44031
44666
|
};
|
|
44032
44667
|
}
|
|
@@ -44109,6 +44744,161 @@ var init_middleware2 = __esm({
|
|
|
44109
44744
|
}
|
|
44110
44745
|
});
|
|
44111
44746
|
|
|
44747
|
+
// ../../packages/server/tw/compression.ts
|
|
44748
|
+
function shouldCompress(contentType, options = {}) {
|
|
44749
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44750
|
+
const ct = contentType.split(";")[0].trim();
|
|
44751
|
+
return opts.contentTypes.includes(ct);
|
|
44752
|
+
}
|
|
44753
|
+
function isLargeEnough(size, options = {}) {
|
|
44754
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44755
|
+
return size >= opts.threshold;
|
|
44756
|
+
}
|
|
44757
|
+
async function compress(data, encoding = "gzip", options = {}) {
|
|
44758
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44759
|
+
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
44760
|
+
if (input.length < opts.threshold) {
|
|
44761
|
+
return input;
|
|
44762
|
+
}
|
|
44763
|
+
try {
|
|
44764
|
+
if (typeof Bun !== "undefined" && Bun.gzipSync) {
|
|
44765
|
+
if (encoding === "gzip") return Bun.gzipSync(input, { level: opts.level });
|
|
44766
|
+
if (encoding === "deflate" && Bun.deflateSync) return Bun.deflateSync(input, { level: opts.level });
|
|
44767
|
+
}
|
|
44768
|
+
} catch {
|
|
44769
|
+
}
|
|
44770
|
+
try {
|
|
44771
|
+
const zlib2 = await import("node:zlib");
|
|
44772
|
+
if (encoding === "gzip") {
|
|
44773
|
+
return zlib2.gzipSync(input, { level: opts.level });
|
|
44774
|
+
}
|
|
44775
|
+
if (encoding === "deflate") {
|
|
44776
|
+
return zlib2.deflateSync(input, { level: opts.level });
|
|
44777
|
+
}
|
|
44778
|
+
if (encoding === "brotli" && zlib2.brotliCompressSync) {
|
|
44779
|
+
return zlib2.brotliCompressSync(input, {
|
|
44780
|
+
params: {
|
|
44781
|
+
[zlib2.constants.BROTLI_PARAM_QUALITY]: opts.level
|
|
44782
|
+
}
|
|
44783
|
+
});
|
|
44784
|
+
}
|
|
44785
|
+
} catch {
|
|
44786
|
+
}
|
|
44787
|
+
return input;
|
|
44788
|
+
}
|
|
44789
|
+
async function decompress(data, encoding = "gzip") {
|
|
44790
|
+
try {
|
|
44791
|
+
if (typeof Bun !== "undefined" && Bun.gunzipSync) {
|
|
44792
|
+
if (encoding === "gzip") return Bun.gunzipSync(data);
|
|
44793
|
+
if (encoding === "deflate" && Bun.inflateSync) return Bun.inflateSync(data);
|
|
44794
|
+
}
|
|
44795
|
+
} catch {
|
|
44796
|
+
}
|
|
44797
|
+
try {
|
|
44798
|
+
const zlib2 = await import("node:zlib");
|
|
44799
|
+
if (encoding === "gzip") return zlib2.gunzipSync(data);
|
|
44800
|
+
if (encoding === "deflate") return zlib2.inflateSync(data);
|
|
44801
|
+
if (encoding === "brotli" && zlib2.brotliDecompressSync) return zlib2.brotliDecompressSync(data);
|
|
44802
|
+
} catch {
|
|
44803
|
+
}
|
|
44804
|
+
return data;
|
|
44805
|
+
}
|
|
44806
|
+
function negotiateEncoding(acceptEncoding) {
|
|
44807
|
+
const encodings = acceptEncoding.split(",").map((e) => e.trim().split(";")[0].trim()).filter(Boolean);
|
|
44808
|
+
if (encodings.includes("br")) return "brotli";
|
|
44809
|
+
if (encodings.includes("gzip")) return "gzip";
|
|
44810
|
+
if (encodings.includes("deflate")) return "deflate";
|
|
44811
|
+
return null;
|
|
44812
|
+
}
|
|
44813
|
+
function createCompressionHeaders(encoding, originalSize, compressedSize) {
|
|
44814
|
+
return {
|
|
44815
|
+
"Content-Encoding": encoding === "brotli" ? "br" : encoding,
|
|
44816
|
+
"Vary": "Accept-Encoding",
|
|
44817
|
+
"X-Original-Size": String(originalSize),
|
|
44818
|
+
"X-Compressed-Size": String(compressedSize),
|
|
44819
|
+
"X-Compression-Ratio": originalSize > 0 ? `${((1 - compressedSize / originalSize) * 100).toFixed(1)}%` : "0%"
|
|
44820
|
+
};
|
|
44821
|
+
}
|
|
44822
|
+
async function compressResponse(body, contentType, acceptEncoding, options = {}) {
|
|
44823
|
+
const input = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
44824
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44825
|
+
if (!shouldCompress(contentType, opts) || !isLargeEnough(input.length, opts)) {
|
|
44826
|
+
return { body: input, headers: {}, compressed: false };
|
|
44827
|
+
}
|
|
44828
|
+
const encoding = negotiateEncoding(acceptEncoding);
|
|
44829
|
+
if (!encoding) {
|
|
44830
|
+
return { body: input, headers: {}, compressed: false };
|
|
44831
|
+
}
|
|
44832
|
+
try {
|
|
44833
|
+
const compressed = await compress(input, encoding, opts);
|
|
44834
|
+
const headers = createCompressionHeaders(encoding, input.length, compressed.length);
|
|
44835
|
+
return { body: compressed, headers, compressed: true };
|
|
44836
|
+
} catch {
|
|
44837
|
+
return { body: input, headers: {}, compressed: false };
|
|
44838
|
+
}
|
|
44839
|
+
}
|
|
44840
|
+
var DEFAULT_OPTIONS4, StreamingCompressor;
|
|
44841
|
+
var init_compression = __esm({
|
|
44842
|
+
"../../packages/server/tw/compression.ts"() {
|
|
44843
|
+
DEFAULT_OPTIONS4 = {
|
|
44844
|
+
level: 6,
|
|
44845
|
+
threshold: 1024,
|
|
44846
|
+
// Don't compress < 1KB
|
|
44847
|
+
brotli: true,
|
|
44848
|
+
contentTypes: [
|
|
44849
|
+
"text/html",
|
|
44850
|
+
"text/css",
|
|
44851
|
+
"text/plain",
|
|
44852
|
+
"text/javascript",
|
|
44853
|
+
"application/javascript",
|
|
44854
|
+
"application/json",
|
|
44855
|
+
"application/xml",
|
|
44856
|
+
"application/xhtml+xml",
|
|
44857
|
+
"image/svg+xml",
|
|
44858
|
+
"font/woff",
|
|
44859
|
+
"font/woff2",
|
|
44860
|
+
"application/wasm"
|
|
44861
|
+
]
|
|
44862
|
+
};
|
|
44863
|
+
StreamingCompressor = class {
|
|
44864
|
+
chunks = [];
|
|
44865
|
+
encoding;
|
|
44866
|
+
options;
|
|
44867
|
+
constructor(encoding = "gzip", options = {}) {
|
|
44868
|
+
this.encoding = encoding;
|
|
44869
|
+
this.options = { ...DEFAULT_OPTIONS4, ...options };
|
|
44870
|
+
}
|
|
44871
|
+
/**
|
|
44872
|
+
* Add a chunk of data to be compressed.
|
|
44873
|
+
*/
|
|
44874
|
+
push(data) {
|
|
44875
|
+
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
44876
|
+
this.chunks.push(input);
|
|
44877
|
+
}
|
|
44878
|
+
/**
|
|
44879
|
+
* Flush all data and return compressed result.
|
|
44880
|
+
*/
|
|
44881
|
+
async flush() {
|
|
44882
|
+
const totalLength = this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
44883
|
+
const combined = new Uint8Array(totalLength);
|
|
44884
|
+
let offset = 0;
|
|
44885
|
+
for (const chunk of this.chunks) {
|
|
44886
|
+
combined.set(chunk, offset);
|
|
44887
|
+
offset += chunk.length;
|
|
44888
|
+
}
|
|
44889
|
+
this.chunks = [];
|
|
44890
|
+
return compress(combined, this.encoding, this.options);
|
|
44891
|
+
}
|
|
44892
|
+
/**
|
|
44893
|
+
* Get the total uncompressed size so far.
|
|
44894
|
+
*/
|
|
44895
|
+
get uncompressedSize() {
|
|
44896
|
+
return this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
44897
|
+
}
|
|
44898
|
+
};
|
|
44899
|
+
}
|
|
44900
|
+
});
|
|
44901
|
+
|
|
44112
44902
|
// ../../packages/server/tw/static/index.ts
|
|
44113
44903
|
import { existsSync as existsSync11, statSync as statSync6 } from "node:fs";
|
|
44114
44904
|
import { join as join15, extname as extname7, normalize as normalize2, resolve as resolve4, sep } from "node:path";
|
|
@@ -44318,7 +45108,7 @@ var init_policy = __esm({
|
|
|
44318
45108
|
"Content-Security-Policy": this.build(),
|
|
44319
45109
|
"X-Content-Type-Options": "nosniff",
|
|
44320
45110
|
"X-Frame-Options": "DENY",
|
|
44321
|
-
|
|
45111
|
+
// BUG 48: x-xss-protection removed (deprecated, harmful in old browsers)
|
|
44322
45112
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
44323
45113
|
"Permissions-Policy": "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()",
|
|
44324
45114
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
@@ -48952,9 +49742,11 @@ function devSecurityHeaders() {
|
|
|
48952
49742
|
removePoweredBy: true
|
|
48953
49743
|
});
|
|
48954
49744
|
}
|
|
48955
|
-
var DEFAULTS2, SecurityHeaders2;
|
|
49745
|
+
var BASELINE_CSP, DEFAULTS2, SecurityHeaders2;
|
|
48956
49746
|
var init_security_headers = __esm({
|
|
48957
49747
|
"../../packages/security/tw/headers/security-headers.ts"() {
|
|
49748
|
+
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.
|
|
49749
|
+
"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'";
|
|
48958
49750
|
DEFAULTS2 = {
|
|
48959
49751
|
hstsMaxAge: 31536e3,
|
|
48960
49752
|
// 1 year
|
|
@@ -48963,7 +49755,9 @@ var init_security_headers = __esm({
|
|
|
48963
49755
|
frameOptions: "SAMEORIGIN",
|
|
48964
49756
|
contentTypeOptions: true,
|
|
48965
49757
|
referrerPolicy: "strict-origin-when-cross-origin",
|
|
48966
|
-
|
|
49758
|
+
// v1.0.8 round 5 (BUG 48): x-xss-protection deprecated since 2019 --
|
|
49759
|
+
// OFF by default now; opt back in with security.xssProtection.
|
|
49760
|
+
xssProtection: void 0,
|
|
48967
49761
|
removePoweredBy: true,
|
|
48968
49762
|
coep: "unsafe-none",
|
|
48969
49763
|
coop: "same-origin",
|
|
@@ -49019,6 +49813,9 @@ var init_security_headers = __esm({
|
|
|
49019
49813
|
if (this.config.serverHeader) {
|
|
49020
49814
|
headers["Server"] = this.config.serverHeader;
|
|
49021
49815
|
}
|
|
49816
|
+
if (this.config.csp !== false && !headers["Content-Security-Policy"]) {
|
|
49817
|
+
headers["Content-Security-Policy"] = typeof this.config.csp === "string" ? this.config.csp : BASELINE_CSP;
|
|
49818
|
+
}
|
|
49022
49819
|
return headers;
|
|
49023
49820
|
}
|
|
49024
49821
|
/** Returns the security headers object. */
|
|
@@ -53229,161 +54026,6 @@ var init_ssr = __esm({
|
|
|
53229
54026
|
}
|
|
53230
54027
|
});
|
|
53231
54028
|
|
|
53232
|
-
// ../../packages/server/tw/compression.ts
|
|
53233
|
-
function shouldCompress(contentType, options = {}) {
|
|
53234
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53235
|
-
const ct = contentType.split(";")[0].trim();
|
|
53236
|
-
return opts.contentTypes.includes(ct);
|
|
53237
|
-
}
|
|
53238
|
-
function isLargeEnough(size, options = {}) {
|
|
53239
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53240
|
-
return size >= opts.threshold;
|
|
53241
|
-
}
|
|
53242
|
-
async function compress(data, encoding = "gzip", options = {}) {
|
|
53243
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53244
|
-
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
53245
|
-
if (input.length < opts.threshold) {
|
|
53246
|
-
return input;
|
|
53247
|
-
}
|
|
53248
|
-
try {
|
|
53249
|
-
if (typeof Bun !== "undefined" && Bun.gzipSync) {
|
|
53250
|
-
if (encoding === "gzip") return Bun.gzipSync(input, { level: opts.level });
|
|
53251
|
-
if (encoding === "deflate" && Bun.deflateSync) return Bun.deflateSync(input, { level: opts.level });
|
|
53252
|
-
}
|
|
53253
|
-
} catch {
|
|
53254
|
-
}
|
|
53255
|
-
try {
|
|
53256
|
-
const zlib2 = await import("node:zlib");
|
|
53257
|
-
if (encoding === "gzip") {
|
|
53258
|
-
return zlib2.gzipSync(input, { level: opts.level });
|
|
53259
|
-
}
|
|
53260
|
-
if (encoding === "deflate") {
|
|
53261
|
-
return zlib2.deflateSync(input, { level: opts.level });
|
|
53262
|
-
}
|
|
53263
|
-
if (encoding === "brotli" && zlib2.brotliCompressSync) {
|
|
53264
|
-
return zlib2.brotliCompressSync(input, {
|
|
53265
|
-
params: {
|
|
53266
|
-
[zlib2.constants.BROTLI_PARAM_QUALITY]: opts.level
|
|
53267
|
-
}
|
|
53268
|
-
});
|
|
53269
|
-
}
|
|
53270
|
-
} catch {
|
|
53271
|
-
}
|
|
53272
|
-
return input;
|
|
53273
|
-
}
|
|
53274
|
-
async function decompress(data, encoding = "gzip") {
|
|
53275
|
-
try {
|
|
53276
|
-
if (typeof Bun !== "undefined" && Bun.gunzipSync) {
|
|
53277
|
-
if (encoding === "gzip") return Bun.gunzipSync(data);
|
|
53278
|
-
if (encoding === "deflate" && Bun.inflateSync) return Bun.inflateSync(data);
|
|
53279
|
-
}
|
|
53280
|
-
} catch {
|
|
53281
|
-
}
|
|
53282
|
-
try {
|
|
53283
|
-
const zlib2 = await import("node:zlib");
|
|
53284
|
-
if (encoding === "gzip") return zlib2.gunzipSync(data);
|
|
53285
|
-
if (encoding === "deflate") return zlib2.inflateSync(data);
|
|
53286
|
-
if (encoding === "brotli" && zlib2.brotliDecompressSync) return zlib2.brotliDecompressSync(data);
|
|
53287
|
-
} catch {
|
|
53288
|
-
}
|
|
53289
|
-
return data;
|
|
53290
|
-
}
|
|
53291
|
-
function negotiateEncoding(acceptEncoding) {
|
|
53292
|
-
const encodings = acceptEncoding.split(",").map((e) => e.trim().split(";")[0].trim()).filter(Boolean);
|
|
53293
|
-
if (encodings.includes("br")) return "brotli";
|
|
53294
|
-
if (encodings.includes("gzip")) return "gzip";
|
|
53295
|
-
if (encodings.includes("deflate")) return "deflate";
|
|
53296
|
-
return null;
|
|
53297
|
-
}
|
|
53298
|
-
function createCompressionHeaders(encoding, originalSize, compressedSize) {
|
|
53299
|
-
return {
|
|
53300
|
-
"Content-Encoding": encoding === "brotli" ? "br" : encoding,
|
|
53301
|
-
"Vary": "Accept-Encoding",
|
|
53302
|
-
"X-Original-Size": String(originalSize),
|
|
53303
|
-
"X-Compressed-Size": String(compressedSize),
|
|
53304
|
-
"X-Compression-Ratio": originalSize > 0 ? `${((1 - compressedSize / originalSize) * 100).toFixed(1)}%` : "0%"
|
|
53305
|
-
};
|
|
53306
|
-
}
|
|
53307
|
-
async function compressResponse(body, contentType, acceptEncoding, options = {}) {
|
|
53308
|
-
const input = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
53309
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53310
|
-
if (!shouldCompress(contentType, opts) || !isLargeEnough(input.length, opts)) {
|
|
53311
|
-
return { body: input, headers: {}, compressed: false };
|
|
53312
|
-
}
|
|
53313
|
-
const encoding = negotiateEncoding(acceptEncoding);
|
|
53314
|
-
if (!encoding) {
|
|
53315
|
-
return { body: input, headers: {}, compressed: false };
|
|
53316
|
-
}
|
|
53317
|
-
try {
|
|
53318
|
-
const compressed = await compress(input, encoding, opts);
|
|
53319
|
-
const headers = createCompressionHeaders(encoding, input.length, compressed.length);
|
|
53320
|
-
return { body: compressed, headers, compressed: true };
|
|
53321
|
-
} catch {
|
|
53322
|
-
return { body: input, headers: {}, compressed: false };
|
|
53323
|
-
}
|
|
53324
|
-
}
|
|
53325
|
-
var DEFAULT_OPTIONS4, StreamingCompressor;
|
|
53326
|
-
var init_compression = __esm({
|
|
53327
|
-
"../../packages/server/tw/compression.ts"() {
|
|
53328
|
-
DEFAULT_OPTIONS4 = {
|
|
53329
|
-
level: 6,
|
|
53330
|
-
threshold: 1024,
|
|
53331
|
-
// Don't compress < 1KB
|
|
53332
|
-
brotli: true,
|
|
53333
|
-
contentTypes: [
|
|
53334
|
-
"text/html",
|
|
53335
|
-
"text/css",
|
|
53336
|
-
"text/plain",
|
|
53337
|
-
"text/javascript",
|
|
53338
|
-
"application/javascript",
|
|
53339
|
-
"application/json",
|
|
53340
|
-
"application/xml",
|
|
53341
|
-
"application/xhtml+xml",
|
|
53342
|
-
"image/svg+xml",
|
|
53343
|
-
"font/woff",
|
|
53344
|
-
"font/woff2",
|
|
53345
|
-
"application/wasm"
|
|
53346
|
-
]
|
|
53347
|
-
};
|
|
53348
|
-
StreamingCompressor = class {
|
|
53349
|
-
chunks = [];
|
|
53350
|
-
encoding;
|
|
53351
|
-
options;
|
|
53352
|
-
constructor(encoding = "gzip", options = {}) {
|
|
53353
|
-
this.encoding = encoding;
|
|
53354
|
-
this.options = { ...DEFAULT_OPTIONS4, ...options };
|
|
53355
|
-
}
|
|
53356
|
-
/**
|
|
53357
|
-
* Add a chunk of data to be compressed.
|
|
53358
|
-
*/
|
|
53359
|
-
push(data) {
|
|
53360
|
-
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
53361
|
-
this.chunks.push(input);
|
|
53362
|
-
}
|
|
53363
|
-
/**
|
|
53364
|
-
* Flush all data and return compressed result.
|
|
53365
|
-
*/
|
|
53366
|
-
async flush() {
|
|
53367
|
-
const totalLength = this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
53368
|
-
const combined = new Uint8Array(totalLength);
|
|
53369
|
-
let offset = 0;
|
|
53370
|
-
for (const chunk of this.chunks) {
|
|
53371
|
-
combined.set(chunk, offset);
|
|
53372
|
-
offset += chunk.length;
|
|
53373
|
-
}
|
|
53374
|
-
this.chunks = [];
|
|
53375
|
-
return compress(combined, this.encoding, this.options);
|
|
53376
|
-
}
|
|
53377
|
-
/**
|
|
53378
|
-
* Get the total uncompressed size so far.
|
|
53379
|
-
*/
|
|
53380
|
-
get uncompressedSize() {
|
|
53381
|
-
return this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
53382
|
-
}
|
|
53383
|
-
};
|
|
53384
|
-
}
|
|
53385
|
-
});
|
|
53386
|
-
|
|
53387
54029
|
// ../../packages/server/tw/edge.ts
|
|
53388
54030
|
function createEdgeHandler(config) {
|
|
53389
54031
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -54102,8 +54744,11 @@ var init_tw6 = __esm({
|
|
|
54102
54744
|
init_tw2();
|
|
54103
54745
|
init_websocket_manager();
|
|
54104
54746
|
init_routing2();
|
|
54747
|
+
init_twm_rules();
|
|
54105
54748
|
init_router();
|
|
54106
54749
|
init_middleware2();
|
|
54750
|
+
init_compression();
|
|
54751
|
+
init_signal_stream();
|
|
54107
54752
|
init_static();
|
|
54108
54753
|
init_tw5();
|
|
54109
54754
|
init_ssr();
|
|
@@ -54152,6 +54797,7 @@ var init_tw6 = __esm({
|
|
|
54152
54797
|
}
|
|
54153
54798
|
ssr = null;
|
|
54154
54799
|
server = null;
|
|
54800
|
+
compressionMode = "off";
|
|
54155
54801
|
isRunning = false;
|
|
54156
54802
|
pluginManager = null;
|
|
54157
54803
|
imageHandler = null;
|
|
@@ -54168,6 +54814,40 @@ var init_tw6 = __esm({
|
|
|
54168
54814
|
const entries = Object.entries(cfgIn.redirects);
|
|
54169
54815
|
cfgIn.redirects = entries.map(([from, to]) => typeof to === "string" ? { from, to } : { from, ...to });
|
|
54170
54816
|
}
|
|
54817
|
+
if (Array.isArray(cfgIn.redirects)) {
|
|
54818
|
+
const REDIRECT_KNOWN = /* @__PURE__ */ new Set(["from", "to", "status", "source", "destination", "permanent"]);
|
|
54819
|
+
let redirectDropped = false;
|
|
54820
|
+
const normalizedRedirects = cfgIn.redirects.map((r) => {
|
|
54821
|
+
if (!r || typeof r !== "object") return r;
|
|
54822
|
+
let touched = false;
|
|
54823
|
+
const mapped = { ...r };
|
|
54824
|
+
if (mapped.from === void 0 && mapped.source !== void 0) {
|
|
54825
|
+
mapped.from = mapped.source;
|
|
54826
|
+
touched = true;
|
|
54827
|
+
}
|
|
54828
|
+
if (mapped.to === void 0 && mapped.destination !== void 0) {
|
|
54829
|
+
mapped.to = mapped.destination;
|
|
54830
|
+
touched = true;
|
|
54831
|
+
}
|
|
54832
|
+
if (mapped.status === void 0 && mapped.permanent !== void 0) {
|
|
54833
|
+
mapped.status = mapped.permanent ? 301 : 302;
|
|
54834
|
+
touched = true;
|
|
54835
|
+
}
|
|
54836
|
+
const unknown = Object.keys(mapped).filter((k) => !REDIRECT_KNOWN.has(k));
|
|
54837
|
+
if (unknown.length > 0) {
|
|
54838
|
+
console.warn("[tw] redirects: unrecognized field" + (unknown.length > 1 ? "s" : "") + " " + unknown.join(", ") + " in entry { from: " + JSON.stringify(mapped.from) + " } -- accepted fields: from, to, status");
|
|
54839
|
+
}
|
|
54840
|
+
if (typeof mapped.from !== "string" || typeof mapped.to !== "string") {
|
|
54841
|
+
console.warn("[tw] redirects: entry missing from/to after normalization -- skipped (" + JSON.stringify(r).slice(0, 80) + ")");
|
|
54842
|
+
redirectDropped = true;
|
|
54843
|
+
return null;
|
|
54844
|
+
}
|
|
54845
|
+
return touched ? mapped : r;
|
|
54846
|
+
}).filter((x) => x !== null || (redirectDropped = true));
|
|
54847
|
+
if (redirectDropped || normalizedRedirects.some((x, i) => x !== cfgIn.redirects[i])) {
|
|
54848
|
+
cfgIn.redirects = normalizedRedirects.filter(Boolean);
|
|
54849
|
+
}
|
|
54850
|
+
}
|
|
54171
54851
|
if (cfgIn.headers && !Array.isArray(cfgIn.headers)) {
|
|
54172
54852
|
const entries = Object.entries(cfgIn.headers);
|
|
54173
54853
|
cfgIn.headers = entries.map(([source, headers]) => ({ source, headers }));
|
|
@@ -54185,6 +54865,12 @@ var init_tw6 = __esm({
|
|
|
54185
54865
|
}
|
|
54186
54866
|
}
|
|
54187
54867
|
this.pluginManager = this.options.plugins ?? null;
|
|
54868
|
+
{
|
|
54869
|
+
const raw = this.options.compression ?? true;
|
|
54870
|
+
if (raw === false || raw === "none" || raw === "off" || raw === "false") this.compressionMode = "off";
|
|
54871
|
+
else if (raw === "gzip") this.compressionMode = "gzip";
|
|
54872
|
+
else this.compressionMode = "brotli";
|
|
54873
|
+
}
|
|
54188
54874
|
try {
|
|
54189
54875
|
this.imageHandler = createImageHandler({
|
|
54190
54876
|
rootDir: this.options.rootDir,
|
|
@@ -54294,31 +54980,72 @@ var init_tw6 = __esm({
|
|
|
54294
54980
|
if (existsSync13(middlewarePath)) {
|
|
54295
54981
|
this.middlewarePath = middlewarePath;
|
|
54296
54982
|
console.log(" Middleware: middleware.twm loaded");
|
|
54983
|
+
try {
|
|
54984
|
+
const src = readFileSync9(middlewarePath, "utf-8");
|
|
54985
|
+
const rules = parseRules(src, "middleware.twm");
|
|
54986
|
+
for (const r of rules) {
|
|
54987
|
+
const conds = [];
|
|
54988
|
+
if (r.userAgent) conds.push("user_agent");
|
|
54989
|
+
if (r.path) conds.push("path");
|
|
54990
|
+
if (r.auth) conds.push("auth");
|
|
54991
|
+
if (r.rateLimit) conds.push("rate_limit");
|
|
54992
|
+
if (r.origin) conds.push("origin");
|
|
54993
|
+
if (r.methods && r.methods.length > 0) conds.push("methods");
|
|
54994
|
+
const label = conds.length > 0 ? conds.join(" + ") : "no conditions -> every matching request gets this response";
|
|
54995
|
+
console.log(` \u2022 "${r.name}" [${label}]`);
|
|
54996
|
+
}
|
|
54997
|
+
} catch {
|
|
54998
|
+
}
|
|
54297
54999
|
}
|
|
54298
55000
|
return this;
|
|
54299
55001
|
}
|
|
54300
55002
|
async start() {
|
|
54301
55003
|
if (this.isRunning) return;
|
|
54302
|
-
const
|
|
54303
|
-
|
|
54304
|
-
|
|
54305
|
-
|
|
54306
|
-
|
|
54307
|
-
|
|
54308
|
-
|
|
54309
|
-
|
|
54310
|
-
|
|
54311
|
-
|
|
54312
|
-
|
|
55004
|
+
const host = this.options.host ?? "0.0.0.0";
|
|
55005
|
+
const wanted = this.options.port ?? 8e3;
|
|
55006
|
+
let port = wanted;
|
|
55007
|
+
let bound = null;
|
|
55008
|
+
let lastErr = null;
|
|
55009
|
+
for (let p = wanted; p < wanted + 10; p++) {
|
|
55010
|
+
try {
|
|
55011
|
+
bound = (typeof Bun !== "undefined" ? Bun : null)?.serve({
|
|
55012
|
+
port: p,
|
|
55013
|
+
hostname: host,
|
|
55014
|
+
fetch: async (request) => {
|
|
55015
|
+
const res = await this.handleRequest(request);
|
|
55016
|
+
let withHeaders = res;
|
|
55017
|
+
if (this.securityHeaders) {
|
|
55018
|
+
try {
|
|
55019
|
+
withHeaders = this.securityHeaders.apply(res);
|
|
55020
|
+
} catch {
|
|
55021
|
+
withHeaders = res;
|
|
55022
|
+
}
|
|
55023
|
+
}
|
|
55024
|
+
return this.compressResponse(withHeaders, request);
|
|
55025
|
+
},
|
|
55026
|
+
error: (err3) => {
|
|
55027
|
+
console.error("Server error:", err3);
|
|
55028
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
54313
55029
|
}
|
|
54314
|
-
}
|
|
54315
|
-
|
|
54316
|
-
|
|
54317
|
-
|
|
54318
|
-
|
|
54319
|
-
|
|
55030
|
+
});
|
|
55031
|
+
port = p;
|
|
55032
|
+
break;
|
|
55033
|
+
} catch (e) {
|
|
55034
|
+
lastErr = e;
|
|
55035
|
+
const msg = String(e?.message ?? e);
|
|
55036
|
+
if (!/address already in use|EADDRINUSE|Failed to start server/i.test(msg)) throw e;
|
|
55037
|
+
if (p < wanted + 9) console.log(` Port ${p} busy \u2014 trying ${p + 1}...`);
|
|
54320
55038
|
}
|
|
54321
|
-
}
|
|
55039
|
+
}
|
|
55040
|
+
if (!bound) {
|
|
55041
|
+
console.error(`
|
|
55042
|
+
ERROR: could not bind ${host} on ports ${wanted}\u2013${wanted + 9}.`);
|
|
55043
|
+
console.error(` ${lastErr?.message ?? lastErr}
|
|
55044
|
+
`);
|
|
55045
|
+
throw lastErr ?? new Error("could not start server");
|
|
55046
|
+
}
|
|
55047
|
+
this.server = bound;
|
|
55048
|
+
this.options.port = port;
|
|
54322
55049
|
this.isRunning = true;
|
|
54323
55050
|
console.log(`
|
|
54324
55051
|
TW Server running at http://${host}:${port}
|
|
@@ -54332,6 +55059,10 @@ var init_tw6 = __esm({
|
|
|
54332
55059
|
// finishes ongoing requests, then closes
|
|
54333
55060
|
async gracefulShutdown(timeout = 5e3) {
|
|
54334
55061
|
this.isRunning = false;
|
|
55062
|
+
try {
|
|
55063
|
+
getSignalHub().closeAll();
|
|
55064
|
+
} catch {
|
|
55065
|
+
}
|
|
54335
55066
|
if (this.server) {
|
|
54336
55067
|
this.server.stop?.(false);
|
|
54337
55068
|
const start = Date.now();
|
|
@@ -54357,6 +55088,35 @@ var init_tw6 = __esm({
|
|
|
54357
55088
|
* - return { headers: {...} } -> headers merged into the final response
|
|
54358
55089
|
* - return null/undefined -> continue
|
|
54359
55090
|
*/
|
|
55091
|
+
/** BUG 7 (v1.0.8): real response compression. `compression` in
|
|
55092
|
+
* tw.config.ts was silently ignored -- the middleware was a no-op and
|
|
55093
|
+
* nothing on the response path ever set Content-Encoding. */
|
|
55094
|
+
async compressResponse(res, request) {
|
|
55095
|
+
if (this.compressionMode === "off") return res;
|
|
55096
|
+
try {
|
|
55097
|
+
if (res.headers.get("content-encoding")) return res;
|
|
55098
|
+
const ct = res.headers.get("content-type") || "";
|
|
55099
|
+
if (ct.includes("text/event-stream")) return res;
|
|
55100
|
+
if (res.__twRawStream) return res;
|
|
55101
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
55102
|
+
if (!isLargeEnough(buf.length)) return new Response(buf, { status: res.status, headers: res.headers });
|
|
55103
|
+
const accept = request.headers.get("accept-encoding") || "";
|
|
55104
|
+
let enc = negotiateEncoding(accept);
|
|
55105
|
+
if (this.compressionMode === "gzip" && enc === "brotli") enc = "gzip";
|
|
55106
|
+
if (!enc) return new Response(buf, { status: res.status, headers: res.headers });
|
|
55107
|
+
const out = await compress(buf, enc);
|
|
55108
|
+
if (out.length >= buf.length) return new Response(buf, { status: res.status, headers: res.headers });
|
|
55109
|
+
const h = new Headers(res.headers);
|
|
55110
|
+
h.set("Content-Encoding", enc === "brotli" ? "br" : enc);
|
|
55111
|
+
h.set("Vary", "Accept-Encoding");
|
|
55112
|
+
h.delete("Content-Length");
|
|
55113
|
+
return new Response(out, { status: res.status, headers: h });
|
|
55114
|
+
} catch {
|
|
55115
|
+
return res;
|
|
55116
|
+
}
|
|
55117
|
+
}
|
|
55118
|
+
/** v1.0.8 round 5 (BUG 39): public now -- serverless adapters (Vercel
|
|
55119
|
+
* functions, edge runtimes) drive the server through this entry. */
|
|
54360
55120
|
async handleRequest(request) {
|
|
54361
55121
|
try {
|
|
54362
55122
|
const u = new URL(request.url);
|
|
@@ -54529,69 +55289,12 @@ var init_tw6 = __esm({
|
|
|
54529
55289
|
* `data: {"v":1,"seq":N,"updates":[...]}` frames.
|
|
54530
55290
|
*/
|
|
54531
55291
|
handleSignalStream(request, url) {
|
|
54532
|
-
const
|
|
54533
|
-
|
|
54534
|
-
|
|
54535
|
-
|
|
54536
|
-
|
|
54537
|
-
|
|
54538
|
-
try {
|
|
54539
|
-
const header = request.headers.get("cookie") ?? "";
|
|
54540
|
-
const m = /(?:^|;\s*)tw_session=([^;]+)/.exec(header);
|
|
54541
|
-
if (m) session = decodeURIComponent(m[1].trim().replace(/^"|"$/g, ""));
|
|
54542
|
-
} catch {
|
|
54543
|
-
}
|
|
54544
|
-
const enc = new TextEncoder();
|
|
54545
|
-
const sse = (payload) => enc.encode("data: " + payload + "\n\n");
|
|
54546
|
-
const stream = new ReadableStream({
|
|
54547
|
-
start(controller) {
|
|
54548
|
-
const client = hub2.register(declared, session);
|
|
54549
|
-
client.send = (payload) => {
|
|
54550
|
-
try {
|
|
54551
|
-
controller.enqueue(sse(payload));
|
|
54552
|
-
} catch {
|
|
54553
|
-
client.close();
|
|
54554
|
-
}
|
|
54555
|
-
};
|
|
54556
|
-
client.close = () => {
|
|
54557
|
-
try {
|
|
54558
|
-
controller.close();
|
|
54559
|
-
} catch {
|
|
54560
|
-
}
|
|
54561
|
-
hub2["clients"];
|
|
54562
|
-
};
|
|
54563
|
-
for (const payload of hub2.connectPayloads(since, declared, session)) {
|
|
54564
|
-
try {
|
|
54565
|
-
controller.enqueue(sse(payload));
|
|
54566
|
-
} catch {
|
|
54567
|
-
break;
|
|
54568
|
-
}
|
|
54569
|
-
}
|
|
54570
|
-
const keepAlive = setInterval(() => {
|
|
54571
|
-
try {
|
|
54572
|
-
controller.enqueue(enc.encode(": keep-alive\n\n"));
|
|
54573
|
-
} catch {
|
|
54574
|
-
clearInterval(keepAlive);
|
|
54575
|
-
}
|
|
54576
|
-
}, 25e3);
|
|
54577
|
-
client.close = () => {
|
|
54578
|
-
clearInterval(keepAlive);
|
|
54579
|
-
try {
|
|
54580
|
-
controller.close();
|
|
54581
|
-
} catch {
|
|
54582
|
-
}
|
|
54583
|
-
};
|
|
54584
|
-
}
|
|
54585
|
-
});
|
|
54586
|
-
return new Response(stream, {
|
|
54587
|
-
status: 200,
|
|
54588
|
-
headers: {
|
|
54589
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
54590
|
-
"Cache-Control": "no-store",
|
|
54591
|
-
"X-Accel-Buffering": "no",
|
|
54592
|
-
Connection: "keep-alive"
|
|
54593
|
-
}
|
|
54594
|
-
});
|
|
55292
|
+
const cfg = this.options.config?.signalStream;
|
|
55293
|
+
return createSignalStream(request, url, cfg ? { maxClients: cfg.maxClients, maxHistory: cfg.maxHistory } : void 0);
|
|
55294
|
+
}
|
|
55295
|
+
async handleClientSignalWrite(request) {
|
|
55296
|
+
const cfg = this.options.config?.signalStream;
|
|
55297
|
+
return writeSignalFromClient(request, { clientWrites: cfg?.clientWrites });
|
|
54595
55298
|
}
|
|
54596
55299
|
/**
|
|
54597
55300
|
* PPR hole fill: render the page fresh and return only the requested
|
|
@@ -54665,15 +55368,33 @@ var init_tw6 = __esm({
|
|
|
54665
55368
|
controller.close();
|
|
54666
55369
|
}
|
|
54667
55370
|
});
|
|
54668
|
-
|
|
55371
|
+
const streamRes = new Response(stream, {
|
|
54669
55372
|
status: 200,
|
|
54670
55373
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
54671
55374
|
});
|
|
55375
|
+
streamRes.__twRawStream = true;
|
|
55376
|
+
return streamRes;
|
|
54672
55377
|
}
|
|
54673
55378
|
async handleRequestInner(request) {
|
|
54674
55379
|
const url = new URL(request.url);
|
|
54675
55380
|
const method = request.method;
|
|
54676
|
-
|
|
55381
|
+
let rawPath = url.pathname;
|
|
55382
|
+
try {
|
|
55383
|
+
const decoded = decodeURIComponent(rawPath);
|
|
55384
|
+
if (decoded !== rawPath) rawPath = decoded;
|
|
55385
|
+
} catch {
|
|
55386
|
+
}
|
|
55387
|
+
let canonical = normalizePathname(rawPath);
|
|
55388
|
+
if (canonical.length > 1 && canonical.endsWith("/")) {
|
|
55389
|
+
canonical = canonical.slice(0, -1);
|
|
55390
|
+
}
|
|
55391
|
+
if (canonical !== rawPath) {
|
|
55392
|
+
return new Response(null, {
|
|
55393
|
+
status: 308,
|
|
55394
|
+
headers: { Location: encodeURI(canonical) + url.search }
|
|
55395
|
+
});
|
|
55396
|
+
}
|
|
55397
|
+
const pathname = canonical;
|
|
54677
55398
|
if (pathname === "/_tw/img" || pathname.startsWith("/_tw/img/")) {
|
|
54678
55399
|
const imgRes = await this.imageHandler.handle(request);
|
|
54679
55400
|
if (imgRes) return imgRes;
|
|
@@ -54681,6 +55402,9 @@ var init_tw6 = __esm({
|
|
|
54681
55402
|
if (pathname === "/_tw/stream") {
|
|
54682
55403
|
return this.handleSignalStream(request, url);
|
|
54683
55404
|
}
|
|
55405
|
+
if (pathname === "/_tw/signal" && method === "POST") {
|
|
55406
|
+
return this.handleClientSignalWrite(request);
|
|
55407
|
+
}
|
|
54684
55408
|
if (pathname === "/_tw/ppr") {
|
|
54685
55409
|
return this.handlePPRHoleFill(url);
|
|
54686
55410
|
}
|
|
@@ -54845,13 +55569,39 @@ __export(twm_loader_exports, {
|
|
|
54845
55569
|
loadTWMModule: () => loadTWMModule,
|
|
54846
55570
|
revalidateRoute: () => revalidateRoute,
|
|
54847
55571
|
shouldMatchMiddleware: () => shouldMatchMiddleware,
|
|
54848
|
-
stripCommentsSafe: () => stripCommentsSafe
|
|
55572
|
+
stripCommentsSafe: () => stripCommentsSafe,
|
|
55573
|
+
validateTWMSyntax: () => validateTWMSyntax
|
|
54849
55574
|
});
|
|
54850
55575
|
import { readFileSync as readFileSync10, existsSync as existsSync14 } from "node:fs";
|
|
54851
55576
|
import { join as join18 } from "node:path";
|
|
54852
55577
|
function clearTWMCache() {
|
|
54853
55578
|
moduleCache.clear();
|
|
54854
55579
|
}
|
|
55580
|
+
function validateTWMSyntax(filePath) {
|
|
55581
|
+
let source;
|
|
55582
|
+
try {
|
|
55583
|
+
source = readFileSync10(filePath, "utf-8");
|
|
55584
|
+
} catch (err3) {
|
|
55585
|
+
return "cannot read: " + (err3?.message ?? err3);
|
|
55586
|
+
}
|
|
55587
|
+
if (isRuleDsl(source)) {
|
|
55588
|
+
try {
|
|
55589
|
+
parseRules(source, filePath);
|
|
55590
|
+
return null;
|
|
55591
|
+
} catch (err3) {
|
|
55592
|
+
return "rule DSL: " + (err3?.message ?? String(err3));
|
|
55593
|
+
}
|
|
55594
|
+
}
|
|
55595
|
+
try {
|
|
55596
|
+
const imports = extractImports(source);
|
|
55597
|
+
const jsCode = compileTwm(source);
|
|
55598
|
+
const codeWithoutImports = stripImports(jsCode);
|
|
55599
|
+
new Function("__tw_unused", codeWithoutImports);
|
|
55600
|
+
return null;
|
|
55601
|
+
} catch (err3) {
|
|
55602
|
+
return (err3?.message ?? String(err3)).replace(/\n/g, " ").slice(0, 200);
|
|
55603
|
+
}
|
|
55604
|
+
}
|
|
54855
55605
|
function stripCommentsSafe(src) {
|
|
54856
55606
|
let out = "";
|
|
54857
55607
|
let i = 0;
|
|
@@ -55089,7 +55839,7 @@ async function loadTWMModule(filePath, rootDir) {
|
|
|
55089
55839
|
console.error("[tw] " + e.message + " (" + filePath + ":" + cm[1] + ")");
|
|
55090
55840
|
}
|
|
55091
55841
|
}
|
|
55092
|
-
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(", ") + " };";
|
|
55842
|
+
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(", ") + " };";
|
|
55093
55843
|
const injectedKeys = Object.keys(injected);
|
|
55094
55844
|
let module = {};
|
|
55095
55845
|
try {
|
|
@@ -55104,6 +55854,8 @@ async function loadTWMModule(filePath, rootDir) {
|
|
|
55104
55854
|
put: H.put || H.PUT || void 0,
|
|
55105
55855
|
delete: H.deleteFn || H.DELETE || void 0,
|
|
55106
55856
|
patch: H.patch || H.PATCH || void 0,
|
|
55857
|
+
// v1.0.8 round 4 (BUG 33): route-level OPTIONS handlers
|
|
55858
|
+
options: H.options || H.OPTIONS || void 0,
|
|
55107
55859
|
middleware: H.middleware || void 0,
|
|
55108
55860
|
generate: H.generate || void 0,
|
|
55109
55861
|
config: H.config || void 0,
|
|
@@ -55239,9 +55991,22 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55239
55991
|
if (rawBody2.length > MAX_BODY_BYTES) {
|
|
55240
55992
|
return { status: 413, json: { ok: false, error: "Payload too large" } };
|
|
55241
55993
|
}
|
|
55242
|
-
|
|
55994
|
+
if (!rawBody2) {
|
|
55995
|
+
parsedBody = {};
|
|
55996
|
+
} else if (__ct.includes("application/x-www-form-urlencoded")) {
|
|
55997
|
+
parsedBody = Object.fromEntries(new URLSearchParams(rawBody2).entries());
|
|
55998
|
+
} else if (__ct.startsWith("text/")) {
|
|
55999
|
+
parsedBody = rawBody2;
|
|
56000
|
+
} else if (__ct.includes("json") || __ct === "") {
|
|
56001
|
+
try {
|
|
56002
|
+
parsedBody = JSON.parse(rawBody2);
|
|
56003
|
+
} catch (e) {
|
|
56004
|
+
return { status: 400, json: { ok: false, error: "Invalid JSON body: " + (e?.message ?? "parse error") } };
|
|
56005
|
+
}
|
|
56006
|
+
} else {
|
|
56007
|
+
return { status: 415, json: { ok: false, error: "Unsupported Content-Type: " + (__ct || "(none)") } };
|
|
56008
|
+
}
|
|
55243
56009
|
} catch {
|
|
55244
|
-
parsedBody = {};
|
|
55245
56010
|
}
|
|
55246
56011
|
} else if (request?.body && typeof request.body === "object") {
|
|
55247
56012
|
parsedBody = request.body;
|
|
@@ -55249,7 +56014,18 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55249
56014
|
let query2 = {};
|
|
55250
56015
|
try {
|
|
55251
56016
|
const u = new URL(request.url);
|
|
55252
|
-
|
|
56017
|
+
for (const [rawKey, value] of u.searchParams.entries()) {
|
|
56018
|
+
const isPhpArray = rawKey.endsWith("[]");
|
|
56019
|
+
const key = isPhpArray ? rawKey.slice(0, -2) : rawKey;
|
|
56020
|
+
const existing = query2[key];
|
|
56021
|
+
if (existing === void 0) {
|
|
56022
|
+
query2[key] = isPhpArray ? [value] : value;
|
|
56023
|
+
} else if (Array.isArray(existing)) {
|
|
56024
|
+
existing.push(value);
|
|
56025
|
+
} else {
|
|
56026
|
+
query2[key] = [existing, value];
|
|
56027
|
+
}
|
|
56028
|
+
}
|
|
55253
56029
|
} catch {
|
|
55254
56030
|
}
|
|
55255
56031
|
const headersObj = typeof request.headers?.entries === "function" ? Object.fromEntries(request.headers.entries()) : request.headers ?? {};
|
|
@@ -55378,9 +56154,17 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55378
56154
|
console.error("[tw] route handler error (" + filePath + "):", err3?.message ?? err3);
|
|
55379
56155
|
return { status: 500, json: { ok: false, error: "Internal Server Error" } };
|
|
55380
56156
|
}
|
|
55381
|
-
if (
|
|
55382
|
-
|
|
55383
|
-
|
|
56157
|
+
if (typeof result === "string") {
|
|
56158
|
+
result = { status: 200, text: result };
|
|
56159
|
+
} else if (!result || typeof result !== "object") {
|
|
56160
|
+
console.error("[tw] route handler returned " + JSON.stringify(result) + " (" + filePath + ")");
|
|
56161
|
+
return {
|
|
56162
|
+
status: 500,
|
|
56163
|
+
json: {
|
|
56164
|
+
ok: false,
|
|
56165
|
+
error: "Unsupported handler return shape: " + (result === null ? "null" : typeof result) + ". Return { status, json|text|html } (or a plain string for text/plain)."
|
|
56166
|
+
}
|
|
56167
|
+
};
|
|
55384
56168
|
}
|
|
55385
56169
|
const finalResult = {
|
|
55386
56170
|
status: typeof result.status === "number" ? result.status : 200,
|
|
@@ -55735,11 +56519,18 @@ async function buildCommand() {
|
|
|
55735
56519
|
} catch {
|
|
55736
56520
|
}
|
|
55737
56521
|
let pageCount = 0;
|
|
56522
|
+
let requestRenderedCount = 0;
|
|
55738
56523
|
let apiCount = 0;
|
|
55739
56524
|
let errorCount = 0;
|
|
55740
56525
|
const isrRoutes = /* @__PURE__ */ new Map();
|
|
55741
56526
|
const pageFiles = [];
|
|
55742
56527
|
collectFiles(homeDir, "page.tw", pageFiles);
|
|
56528
|
+
for (const pf of pageFiles) {
|
|
56529
|
+
if (existsSync16(join20(dirname6(pf), "index.tw"))) {
|
|
56530
|
+
console.log(" \x1B[33m!\x1B[0m " + dirname6(pf).replace(homeDir, "").replace(/^\//, "") + "/ has BOTH page.tw and index.tw");
|
|
56531
|
+
console.log(" index.tw wins this route -- delete one of them");
|
|
56532
|
+
}
|
|
56533
|
+
}
|
|
55743
56534
|
begin("compile", pageFiles.length);
|
|
55744
56535
|
const htmlOutputs = [];
|
|
55745
56536
|
_compilerMod.clearCssCapture?.();
|
|
@@ -55757,11 +56548,20 @@ async function buildCommand() {
|
|
|
55757
56548
|
const pmod = await loadTWMModule2(paramsTwm, rootDir);
|
|
55758
56549
|
if (typeof pmod.generate === "function") {
|
|
55759
56550
|
const arr = pmod.generate();
|
|
55760
|
-
if (Array.isArray(arr) && arr.length > 0)
|
|
55761
|
-
const
|
|
55762
|
-
|
|
55763
|
-
|
|
55764
|
-
|
|
56551
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
56552
|
+
const dynNames = pageFile.split("/").map((seg) => /^\[([^\]]+)\]$/.exec(seg)?.[1]).filter(Boolean);
|
|
56553
|
+
paramSets = arr.map((x) => {
|
|
56554
|
+
if (x !== null && typeof x !== "object") {
|
|
56555
|
+
const o2 = {};
|
|
56556
|
+
const key = dynNames[0] ?? "slug";
|
|
56557
|
+
o2[key] = String(x);
|
|
56558
|
+
return o2;
|
|
56559
|
+
}
|
|
56560
|
+
const o = {};
|
|
56561
|
+
for (const [k, v] of Object.entries(x)) o[k] = String(v);
|
|
56562
|
+
return o;
|
|
56563
|
+
});
|
|
56564
|
+
}
|
|
55765
56565
|
}
|
|
55766
56566
|
} catch (e) {
|
|
55767
56567
|
console.log(" Warning: params.twm failed: " + e.message);
|
|
@@ -55812,6 +56612,7 @@ async function buildCommand() {
|
|
|
55812
56612
|
}
|
|
55813
56613
|
const hasExplicitParams = paramSets.length > 1 || Object.keys(paramSets[0] ?? {}).length > 0;
|
|
55814
56614
|
if ((renderMode === "ssr" || renderMode === "stream") && !hasExplicitParams) {
|
|
56615
|
+
requestRenderedCount++;
|
|
55815
56616
|
console.log(" \x1B[36m~\x1B[0m " + routePath + " [" + renderMode + ": rendered at request time]");
|
|
55816
56617
|
try {
|
|
55817
56618
|
const src = readFileSync12(pageFile, "utf-8");
|
|
@@ -55822,6 +56623,29 @@ async function buildCommand() {
|
|
|
55822
56623
|
for (const d of errs.slice(0, 5)) console.log(" " + d.line + ":" + d.col + " " + d.message);
|
|
55823
56624
|
errorCount++;
|
|
55824
56625
|
}
|
|
56626
|
+
const warns = (r.diagnostics || []).filter((d) => d.severity !== "error");
|
|
56627
|
+
if (warns.length > 0) {
|
|
56628
|
+
console.log(" \x1B[33m" + warns.length + " warning" + (warns.length === 1 ? "" : "s") + "\x1B[0m");
|
|
56629
|
+
for (const d of warns.slice(0, 3)) {
|
|
56630
|
+
console.log(" " + d.line + ":" + d.col + " " + d.code + " " + d.message);
|
|
56631
|
+
}
|
|
56632
|
+
if (warns.length > 3) console.log(" ... " + (warns.length - 3) + " more (tw check)");
|
|
56633
|
+
}
|
|
56634
|
+
{
|
|
56635
|
+
const used = /* @__PURE__ */ new Set();
|
|
56636
|
+
collectComponentUsages(r.ast, used);
|
|
56637
|
+
const BUILTIN_ALIASES = /* @__PURE__ */ new Set(["Link", "RouterLink", "Image", "optImage", "Suspense"]);
|
|
56638
|
+
const unknownSsr = [...used].filter((t2) => !BUILTIN_ALIASES.has(t2) && !registeredComponentNames.has(t2));
|
|
56639
|
+
if (unknownSsr.length > 0) {
|
|
56640
|
+
const importSpecs = /* @__PURE__ */ new Map();
|
|
56641
|
+
for (const im of src.matchAll(/import\s+([A-Za-z_]\w*)\s+from\s+["']([^"']+)["']/g)) {
|
|
56642
|
+
importSpecs.set(im[1], im[2]);
|
|
56643
|
+
}
|
|
56644
|
+
const hints = unknownSsr.map((t2) => importSpecs.has(t2) ? `<${t2}> (imported from ${importSpecs.get(t2)} -- file not found)` : `<${t2}> (no components/${t2}.tw found)`);
|
|
56645
|
+
console.log(" \x1B[31m\u2717 " + routePath + ": unknown component" + (unknownSsr.length > 1 ? "s" : "") + " " + hints.join(", ") + "\x1B[0m");
|
|
56646
|
+
errorCount++;
|
|
56647
|
+
}
|
|
56648
|
+
}
|
|
55825
56649
|
} catch (e) {
|
|
55826
56650
|
console.log(" \x1B[33m! " + routePath + ": " + (e && e.message ? e.message : e) + "\x1B[0m");
|
|
55827
56651
|
errorCount++;
|
|
@@ -55840,7 +56664,12 @@ async function buildCommand() {
|
|
|
55840
56664
|
const BUILTIN_ALIASES = /* @__PURE__ */ new Set(["Link", "RouterLink", "Image", "optImage", "Suspense"]);
|
|
55841
56665
|
const unknown = [...used].filter((t2) => !BUILTIN_ALIASES.has(t2) && !registeredComponentNames.has(t2));
|
|
55842
56666
|
if (unknown.length > 0) {
|
|
55843
|
-
|
|
56667
|
+
const importSpecs = /* @__PURE__ */ new Map();
|
|
56668
|
+
for (const im of source.matchAll(/import\s+([A-Za-z_]\w*)\s+from\s+["']([^"']+)["']/g)) {
|
|
56669
|
+
importSpecs.set(im[1], im[2]);
|
|
56670
|
+
}
|
|
56671
|
+
const hints = unknown.map((t2) => importSpecs.has(t2) ? `<${t2}> (imported from ${importSpecs.get(t2)} -- file not found)` : `<${t2}> (no components/${t2}.tw found)`);
|
|
56672
|
+
console.error(" \x1B[31m\u2717 " + pageFile.replace(rootDir + "/", "") + ": unknown component" + (unknown.length > 1 ? "s" : "") + " " + hints.join(", ") + " -- create the file or fix the import\x1B[0m");
|
|
55844
56673
|
process.exit(1);
|
|
55845
56674
|
}
|
|
55846
56675
|
}
|
|
@@ -55932,9 +56761,12 @@ async function buildCommand() {
|
|
|
55932
56761
|
if (pu) clientTag += `<script defer src="/${pu}"></script>
|
|
55933
56762
|
`;
|
|
55934
56763
|
}
|
|
55935
|
-
const
|
|
55936
|
-
|
|
55937
|
-
|
|
56764
|
+
const needsClient = interactive || pageMods.deps.length > 0 || pageMods.own.length > 0 || renderMode === "csr" || renderMode === "ppr" || renderMode === "stream" || renderMode === "signalStream";
|
|
56765
|
+
if (needsClient) {
|
|
56766
|
+
const runtimeTag = `<script defer src="/__tw_runtime.js"></script>`;
|
|
56767
|
+
html = html.replace("</body>", " " + clientTag + runtimeTag + "\n</body>");
|
|
56768
|
+
needsRuntime = true;
|
|
56769
|
+
}
|
|
55938
56770
|
}
|
|
55939
56771
|
if (renderMode === "csr") {
|
|
55940
56772
|
html = csrifyHtml(html);
|
|
@@ -55989,6 +56821,14 @@ async function buildCommand() {
|
|
|
55989
56821
|
} else {
|
|
55990
56822
|
console.log(" \x1B[32mOK\x1B[0m " + displayPath + " -> " + (outRoute === "" ? ".tw/index.html" : ".tw/" + outRoute + "/index.html"));
|
|
55991
56823
|
}
|
|
56824
|
+
const warnings = result.diagnostics.filter((d) => d.severity !== "error");
|
|
56825
|
+
if (warnings.length > 0) {
|
|
56826
|
+
console.log(" \x1B[33m" + warnings.length + " warning" + (warnings.length === 1 ? "" : "s") + "\x1B[0m");
|
|
56827
|
+
for (const d of warnings.slice(0, 3)) {
|
|
56828
|
+
console.log(" " + d.line + ":" + d.col + " " + d.code + " " + d.message);
|
|
56829
|
+
}
|
|
56830
|
+
if (warnings.length > 3) console.log(" ... " + (warnings.length - 3) + " more (tw check)");
|
|
56831
|
+
}
|
|
55992
56832
|
pageCount++;
|
|
55993
56833
|
} catch (err3) {
|
|
55994
56834
|
console.log(" \x1B[31mX " + routePath + ": " + err3.message + "\x1B[0m");
|
|
@@ -56007,8 +56847,16 @@ async function buildCommand() {
|
|
|
56007
56847
|
const outputDir = routeName === "" ? outDir : join20(outDir, routeName);
|
|
56008
56848
|
mkdirSync6(outputDir, { recursive: true });
|
|
56009
56849
|
copyFileSync2(twmFile, join20(outputDir, "route.twm"));
|
|
56010
|
-
|
|
56011
|
-
|
|
56850
|
+
const { validateTWMSyntax: validateTWMSyntax2 } = await Promise.resolve().then(() => (init_twm_loader(), twm_loader_exports));
|
|
56851
|
+
const syntaxError = validateTWMSyntax2(twmFile);
|
|
56852
|
+
if (syntaxError) {
|
|
56853
|
+
console.log(" \x1B[31mX\x1B[0m /" + routeName + " [api] -- .twm syntax error");
|
|
56854
|
+
console.log(" " + twmFile.replace(rootDir + "/", "") + ": " + syntaxError);
|
|
56855
|
+
errorCount++;
|
|
56856
|
+
} else {
|
|
56857
|
+
console.log(" \x1B[32mOK\x1B[0m /" + routeName + " [api]");
|
|
56858
|
+
apiCount++;
|
|
56859
|
+
}
|
|
56012
56860
|
}
|
|
56013
56861
|
if (existsSync16(libDir)) {
|
|
56014
56862
|
copyDir2(libDir, join20(outDir, "lib"));
|
|
@@ -56087,7 +56935,7 @@ async function buildCommand() {
|
|
|
56087
56935
|
}
|
|
56088
56936
|
}
|
|
56089
56937
|
console.log("\n \x1B[32mBuild complete!\x1B[0m");
|
|
56090
|
-
console.log(" Pages: " + pageCount + " compiled");
|
|
56938
|
+
console.log(" Pages: " + pageCount + " compiled" + (requestRenderedCount > 0 ? " (+" + requestRenderedCount + " rendered at request time)" : ""));
|
|
56091
56939
|
if (pageCount === 0) console.warn(" [!] No static pages were compiled -- every page is SSR/API. Static hosts (Vercel/Cloudflare Pages output) will serve an empty site. Use `render static` or check your render modes.");
|
|
56092
56940
|
console.log(" APIs: " + apiCount + " routes");
|
|
56093
56941
|
if (isrRoutes.size > 0) {
|
|
@@ -60191,15 +61039,37 @@ ${name}/
|
|
|
60191
61039
|
async function createVercelJson(dir) {
|
|
60192
61040
|
const config = {
|
|
60193
61041
|
buildCommand: "npx tw build",
|
|
60194
|
-
|
|
60195
|
-
|
|
60196
|
-
|
|
61042
|
+
installCommand: "npm install",
|
|
61043
|
+
functions: {
|
|
61044
|
+
"api/tw-server.ts": { memory: 512 }
|
|
61045
|
+
},
|
|
61046
|
+
rewrites: [
|
|
61047
|
+
{ source: "/(.*)", destination: "/api/tw-server" }
|
|
61048
|
+
]
|
|
60197
61049
|
};
|
|
60198
61050
|
await writeFile(join(dir, "vercel.json"), JSON.stringify(config, null, 2));
|
|
60199
61051
|
console.log(" \u2713 vercel.json");
|
|
61052
|
+
const fnDir = join(dir, "api");
|
|
61053
|
+
await mkdir(fnDir, { recursive: true });
|
|
61054
|
+
await writeFile(join(fnDir, "tw-server.ts"), [
|
|
61055
|
+
"// TW Framework -- Vercel serverless entry (v1.0.8 round 5, BUG 39).",
|
|
61056
|
+
"// Runs the full TW server (SSR pages + .twm API routes) as a Vercel",
|
|
61057
|
+
"// function. vercel.json rewrites every route here.",
|
|
61058
|
+
'import { TWServer } from "tw-framework/server";',
|
|
61059
|
+
"",
|
|
61060
|
+
"const server = new TWServer({",
|
|
61061
|
+
" rootDir: process.cwd(),",
|
|
61062
|
+
"});",
|
|
61063
|
+
"",
|
|
61064
|
+
"export default async function handler(request: Request): Promise<Response> {",
|
|
61065
|
+
" return server.handleRequest(request);",
|
|
61066
|
+
"}",
|
|
61067
|
+
""
|
|
61068
|
+
].join("\n"));
|
|
61069
|
+
console.log(" \u2713 api/tw-server.ts");
|
|
60200
61070
|
}
|
|
60201
61071
|
async function createDefaultProject(dir, name) {
|
|
60202
|
-
const T = (s) => s;
|
|
61072
|
+
const T = (s) => s.split("${name}").join(name);
|
|
60203
61073
|
for (const d of [
|
|
60204
61074
|
["home"],
|
|
60205
61075
|
["home", "about"],
|
|
@@ -60222,8 +61092,6 @@ html {
|
|
|
60222
61092
|
head {
|
|
60223
61093
|
meta charset "utf-8"
|
|
60224
61094
|
meta name "viewport" content "width=device-width, initial-scale=1"
|
|
60225
|
-
meta name "description" content "{page.description}"
|
|
60226
|
-
title "{page.title} -- ${"$"}{name}"
|
|
60227
61095
|
}
|
|
60228
61096
|
body {
|
|
60229
61097
|
header.sitenav {
|
|
@@ -60559,6 +61427,10 @@ a:hover { td underline }
|
|
|
60559
61427
|
// This sample blocks known bot user agents everywhere. Guards are
|
|
60560
61428
|
// fail-closed: a matching request that fails the condition gets the
|
|
60561
61429
|
// rule's response, and no page code ever runs.
|
|
61430
|
+
//
|
|
61431
|
+
// Note: a rule with ONLY match + response (no condition blocks) applies
|
|
61432
|
+
// to EVERY matching request -- e.g. a site-wide maintenance page. tw serve
|
|
61433
|
+
// lists each rule at startup and flags those unconditional ones.
|
|
60562
61434
|
|
|
60563
61435
|
rule "block bots" {
|
|
60564
61436
|
match "/**"
|
|
@@ -61522,6 +62394,7 @@ async function devCommand() {
|
|
|
61522
62394
|
}
|
|
61523
62395
|
let devRedirects = [];
|
|
61524
62396
|
let devCfgHeaders = [];
|
|
62397
|
+
let devSigCfg = void 0;
|
|
61525
62398
|
try {
|
|
61526
62399
|
const cfgFile = join19(rootDir, "tw.config.ts");
|
|
61527
62400
|
if (existsSync15(cfgFile)) {
|
|
@@ -61534,12 +62407,24 @@ async function devCommand() {
|
|
|
61534
62407
|
if (cfg?.headers) {
|
|
61535
62408
|
devCfgHeaders = Array.isArray(cfg.headers) ? cfg.headers : Object.entries(cfg.headers).map(([source, headers]) => ({ source, headers }));
|
|
61536
62409
|
}
|
|
62410
|
+
if (cfg?.signalStream) devSigCfg = cfg.signalStream;
|
|
61537
62411
|
}
|
|
61538
62412
|
} catch {
|
|
61539
62413
|
}
|
|
61540
62414
|
const devPathMatch = (pattern, pathname) => pattern === pathname || pattern.endsWith("/**") && pathname.startsWith(pattern.slice(0, -2) + "/") || pattern.endsWith("/") && pathname.startsWith(pattern) || pattern === "/**" && pathname.startsWith("/");
|
|
61541
62415
|
async function devFetchInner(req) {
|
|
61542
62416
|
const url = new URL(req.url);
|
|
62417
|
+
try {
|
|
62418
|
+
let rawPath = url.pathname;
|
|
62419
|
+
const decoded = decodeURIComponent(rawPath);
|
|
62420
|
+
if (decoded !== rawPath) rawPath = decoded;
|
|
62421
|
+
let canonical = rawPath.replace(/\/{2,}/g, "/");
|
|
62422
|
+
if (canonical.length > 1 && canonical.endsWith("/")) canonical = canonical.slice(0, -1);
|
|
62423
|
+
if (canonical !== rawPath) {
|
|
62424
|
+
return new Response(null, { status: 308, headers: { Location: encodeURI(canonical) + url.search } });
|
|
62425
|
+
}
|
|
62426
|
+
} catch {
|
|
62427
|
+
}
|
|
61543
62428
|
const path = url.pathname;
|
|
61544
62429
|
const time = (/* @__PURE__ */ new Date()).toISOString().split("T")[1].split(".")[0];
|
|
61545
62430
|
rescanComponentsIfNeeded(rootDir);
|
|
@@ -61819,56 +62704,108 @@ async function devCommand() {
|
|
|
61819
62704
|
headers: { "Content-Type": "text/html" }
|
|
61820
62705
|
});
|
|
61821
62706
|
}
|
|
61822
|
-
|
|
61823
|
-
|
|
61824
|
-
|
|
61825
|
-
|
|
61826
|
-
|
|
61827
|
-
|
|
61828
|
-
|
|
61829
|
-
|
|
61830
|
-
|
|
61831
|
-
|
|
61832
|
-
|
|
61833
|
-
|
|
61834
|
-
|
|
61835
|
-
|
|
61836
|
-
|
|
61837
|
-
if (
|
|
61838
|
-
|
|
61839
|
-
|
|
61840
|
-
|
|
61841
|
-
|
|
61842
|
-
}
|
|
61843
|
-
|
|
61844
|
-
|
|
61845
|
-
|
|
61846
|
-
|
|
61847
|
-
|
|
61848
|
-
|
|
61849
|
-
|
|
62707
|
+
async function devHandle(req) {
|
|
62708
|
+
{
|
|
62709
|
+
const u3 = new URL(req.url);
|
|
62710
|
+
if (u3.pathname === "/_tw/stream") {
|
|
62711
|
+
const { createSignalStream: createSignalStream2 } = await Promise.resolve().then(() => (init_signal_stream(), signal_stream_exports));
|
|
62712
|
+
return createSignalStream2(req, u3, devSigCfg ? { maxClients: devSigCfg.maxClients, maxHistory: devSigCfg.maxHistory } : void 0);
|
|
62713
|
+
}
|
|
62714
|
+
if (u3.pathname === "/_tw/signal" && req.method === "POST") {
|
|
62715
|
+
const { writeSignalFromClient: writeSignalFromClient2 } = await Promise.resolve().then(() => (init_signal_stream(), signal_stream_exports));
|
|
62716
|
+
return writeSignalFromClient2(req, { clientWrites: devSigCfg?.clientWrites });
|
|
62717
|
+
}
|
|
62718
|
+
}
|
|
62719
|
+
const reqUrl = new URL(req.url);
|
|
62720
|
+
for (const r of devRedirects) {
|
|
62721
|
+
if (!r?.from || !r?.to) continue;
|
|
62722
|
+
if (devPathMatch(r.from, reqUrl.pathname)) {
|
|
62723
|
+
const status = [301, 302, 307, 308].includes(r.status) ? r.status : 301;
|
|
62724
|
+
let loc = r.to.startsWith("http") ? r.to : new URL(r.to, req.url).toString();
|
|
62725
|
+
if (reqUrl.search && !loc.includes("?")) loc += reqUrl.search;
|
|
62726
|
+
return new Response(null, { status, headers: { Location: loc } });
|
|
62727
|
+
}
|
|
62728
|
+
}
|
|
62729
|
+
const mwHeaders = await runUserMiddleware(req);
|
|
62730
|
+
if (mwHeaders instanceof Response) return mwHeaders;
|
|
62731
|
+
const u = new URL(req.url);
|
|
62732
|
+
if (u.pathname === "/_tw/img" || u.pathname.startsWith("/_tw/img/")) {
|
|
62733
|
+
const imgRes = await devImageHandler?.handle(req);
|
|
62734
|
+
if (imgRes) return imgRes;
|
|
62735
|
+
}
|
|
62736
|
+
const response = await devFetchInner(req);
|
|
62737
|
+
if (mwHeaders && typeof mwHeaders === "object") {
|
|
62738
|
+
for (const key of Object.keys(mwHeaders)) {
|
|
62739
|
+
try {
|
|
62740
|
+
if (!response.headers.has(key)) response.headers.set(key, mwHeaders[key]);
|
|
62741
|
+
} catch {
|
|
61850
62742
|
}
|
|
61851
62743
|
}
|
|
61852
|
-
|
|
61853
|
-
|
|
61854
|
-
|
|
61855
|
-
|
|
61856
|
-
|
|
61857
|
-
|
|
61858
|
-
|
|
61859
|
-
|
|
62744
|
+
}
|
|
62745
|
+
const u2 = new URL(req.url);
|
|
62746
|
+
for (const h of devCfgHeaders) {
|
|
62747
|
+
if (h?.source && h?.headers && devPathMatch(h.source, u2.pathname)) {
|
|
62748
|
+
for (const key of Object.keys(h.headers)) {
|
|
62749
|
+
try {
|
|
62750
|
+
if (!response.headers.has(key)) response.headers.set(key, String(h.headers[key]).replace(/[\r\n]/g, ""));
|
|
62751
|
+
} catch {
|
|
61860
62752
|
}
|
|
61861
62753
|
}
|
|
61862
62754
|
}
|
|
61863
|
-
|
|
61864
|
-
|
|
61865
|
-
|
|
61866
|
-
|
|
61867
|
-
|
|
62755
|
+
}
|
|
62756
|
+
if (devSecurityHeaders2) {
|
|
62757
|
+
try {
|
|
62758
|
+
return devSecurityHeaders2.apply(response);
|
|
62759
|
+
} catch {
|
|
61868
62760
|
}
|
|
61869
|
-
return response;
|
|
61870
62761
|
}
|
|
61871
|
-
|
|
62762
|
+
return response;
|
|
62763
|
+
}
|
|
62764
|
+
if (typeof Bun !== "undefined" && typeof Bun.serve === "function") {
|
|
62765
|
+
Bun.serve({ port, hostname: host, fetch: devHandle });
|
|
62766
|
+
} else {
|
|
62767
|
+
const { createServer } = await import("node:http");
|
|
62768
|
+
const srv = createServer(async (nreq, nres) => {
|
|
62769
|
+
try {
|
|
62770
|
+
const hostHdr = String(nreq.headers.host ?? `${host}:${port}`);
|
|
62771
|
+
const chunks = [];
|
|
62772
|
+
for await (const c of nreq) chunks.push(Buffer.from(c));
|
|
62773
|
+
const raw = nreq.method === "GET" || nreq.method === "HEAD" ? void 0 : Buffer.concat(chunks);
|
|
62774
|
+
const request = new Request(`http://${hostHdr}${nreq.url}`, {
|
|
62775
|
+
method: nreq.method,
|
|
62776
|
+
headers: nreq.headers,
|
|
62777
|
+
body: raw && raw.length > 0 ? raw : void 0
|
|
62778
|
+
});
|
|
62779
|
+
const resp = await devHandle(request);
|
|
62780
|
+
const out = Buffer.from(await resp.arrayBuffer());
|
|
62781
|
+
const headers = {};
|
|
62782
|
+
resp.headers.forEach((v, k) => {
|
|
62783
|
+
headers[k] = v;
|
|
62784
|
+
});
|
|
62785
|
+
nres.writeHead(resp.status, headers);
|
|
62786
|
+
nres.end(out);
|
|
62787
|
+
} catch (e) {
|
|
62788
|
+
nres.writeHead(500, { "Content-Type": "text/plain" });
|
|
62789
|
+
nres.end("Internal dev server error: " + (e?.message ?? e));
|
|
62790
|
+
}
|
|
62791
|
+
});
|
|
62792
|
+
await new Promise((r) => srv.listen(port, host, () => r()));
|
|
62793
|
+
console.log(" [tw] runtime: node (Bun not found \u2014 using node:http)");
|
|
62794
|
+
}
|
|
62795
|
+
try {
|
|
62796
|
+
const { watch: watch2 } = await import("node:fs");
|
|
62797
|
+
const seen = /* @__PURE__ */ new Set();
|
|
62798
|
+
watch2(rootDir, { recursive: true }, (_t, f) => {
|
|
62799
|
+
const name = String(f);
|
|
62800
|
+
if (name.includes("node_modules") || name.includes(".git") || name.split("/").includes(".tw")) return;
|
|
62801
|
+
if (seen.has(name)) return;
|
|
62802
|
+
seen.add(name);
|
|
62803
|
+
const t2 = setTimeout(() => seen.delete(name), 400);
|
|
62804
|
+
if (typeof t2?.unref === "function") t2.unref();
|
|
62805
|
+
console.log(` [tw] ${name} changed -- reload the browser`);
|
|
62806
|
+
});
|
|
62807
|
+
} catch {
|
|
62808
|
+
}
|
|
61872
62809
|
console.log(" Server running! Open http://" + host + ":" + port + " in your browser.");
|
|
61873
62810
|
console.log(" Press Ctrl+C to stop.\n");
|
|
61874
62811
|
}
|
|
@@ -61881,6 +62818,25 @@ import { join as join22 } from "node:path";
|
|
|
61881
62818
|
import { existsSync as existsSync18 } from "node:fs";
|
|
61882
62819
|
async function serveCommand() {
|
|
61883
62820
|
const args = process.argv.slice(3);
|
|
62821
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
62822
|
+
console.log(`
|
|
62823
|
+
tw serve \u2014 start the production server
|
|
62824
|
+
|
|
62825
|
+
Usage:
|
|
62826
|
+
tw serve [--port <number>] [--host <address>]
|
|
62827
|
+
|
|
62828
|
+
Options:
|
|
62829
|
+
--port <number> port to listen on (default: 8000; tw.config.ts
|
|
62830
|
+
server.port and the PORT env var also work,
|
|
62831
|
+
in that order of precedence)
|
|
62832
|
+
--host <address> bind address (default: 0.0.0.0)
|
|
62833
|
+
-h, --help show this help
|
|
62834
|
+
|
|
62835
|
+
Requires a build first (tw build); serves .tw/ output with SSR,
|
|
62836
|
+
middleware and API routes.
|
|
62837
|
+
`);
|
|
62838
|
+
process.exit(0);
|
|
62839
|
+
}
|
|
61884
62840
|
let port = null;
|
|
61885
62841
|
let host = "0.0.0.0";
|
|
61886
62842
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -61895,8 +62851,8 @@ async function serveCommand() {
|
|
|
61895
62851
|
const envPath = join22(rootDir, ".env");
|
|
61896
62852
|
if (existsSync18(envPath)) {
|
|
61897
62853
|
try {
|
|
61898
|
-
const { readFileSync:
|
|
61899
|
-
for (const line of
|
|
62854
|
+
const { readFileSync: readFileSync20 } = await import("node:fs");
|
|
62855
|
+
for (const line of readFileSync20(envPath, "utf-8").split("\n")) {
|
|
61900
62856
|
const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/.exec(line);
|
|
61901
62857
|
if (m && !(m[1] in process.env)) {
|
|
61902
62858
|
process.env[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
@@ -61942,7 +62898,7 @@ async function serveCommand() {
|
|
|
61942
62898
|
staticDir: outDir,
|
|
61943
62899
|
config,
|
|
61944
62900
|
...config?.rateLimit ? { rateLimit: config.rateLimit } : {},
|
|
61945
|
-
...
|
|
62901
|
+
...config?.compression !== void 0 || config?.server?.compression !== void 0 ? { compression: config?.compression ?? config?.server?.compression } : {},
|
|
61946
62902
|
...pluginManager && pluginManager.size() > 0 ? { plugins: pluginManager } : {},
|
|
61947
62903
|
...config?.images ? { images: config.images } : {},
|
|
61948
62904
|
...config?.security ? { security: config.security } : {}
|
|
@@ -62221,7 +63177,7 @@ async function adapterCommand() {
|
|
|
62221
63177
|
// tw/commands/test.ts
|
|
62222
63178
|
import { join as join27 } from "node:path";
|
|
62223
63179
|
import { createRequire as _cr } from "node:module";
|
|
62224
|
-
import { existsSync as existsSync22, readdirSync as readdirSync9, statSync as statSync9 } from "node:fs";
|
|
63180
|
+
import { existsSync as existsSync22, readdirSync as readdirSync9, statSync as statSync9, readFileSync as readFileSync15 } from "node:fs";
|
|
62225
63181
|
var _require = _cr(import.meta.url);
|
|
62226
63182
|
function collectTests(dir, out) {
|
|
62227
63183
|
if (!existsSync22(dir)) return;
|
|
@@ -62277,6 +63233,17 @@ async function testCommand() {
|
|
|
62277
63233
|
}
|
|
62278
63234
|
}
|
|
62279
63235
|
console.log("\n tw test -- running " + files.length + " test file" + (files.length === 1 ? "" : "s") + "\n");
|
|
63236
|
+
for (const f of files) {
|
|
63237
|
+
try {
|
|
63238
|
+
const src = readFileSync15(f, "utf-8");
|
|
63239
|
+
const hasBunTest = src.includes("bun:test");
|
|
63240
|
+
const hasTestCall = /\b(test|it|describe)\s*\(/.test(src);
|
|
63241
|
+
if (!hasBunTest && !hasTestCall) {
|
|
63242
|
+
console.log(" \x1B[33m! " + f.replace(rootDir + "/", "") + ' defined no tests -- only bun:test format runs (import { test, expect } from "bun:test")\x1B[0m');
|
|
63243
|
+
}
|
|
63244
|
+
} catch {
|
|
63245
|
+
}
|
|
63246
|
+
}
|
|
62280
63247
|
if (files.length === 0) {
|
|
62281
63248
|
console.log(" No tests found. Create tests/your-name.test.ts:");
|
|
62282
63249
|
console.log(`
|
|
@@ -62309,8 +63276,8 @@ async function testCommand() {
|
|
|
62309
63276
|
|
|
62310
63277
|
// tw/commands/check.ts
|
|
62311
63278
|
init_tw();
|
|
62312
|
-
import { join as join28 } from "node:path";
|
|
62313
|
-
import { existsSync as existsSync23, readFileSync as
|
|
63279
|
+
import { join as join28, dirname as dirname8 } from "node:path";
|
|
63280
|
+
import { existsSync as existsSync23, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync10 } from "node:fs";
|
|
62314
63281
|
async function checkCommand() {
|
|
62315
63282
|
const args = process.argv.slice(3);
|
|
62316
63283
|
const rootDir = process.cwd();
|
|
@@ -62332,7 +63299,7 @@ async function checkCommand() {
|
|
|
62332
63299
|
for (const filePath of twFiles) {
|
|
62333
63300
|
const relativePath = filePath.replace(rootDir + "/", "");
|
|
62334
63301
|
try {
|
|
62335
|
-
const source =
|
|
63302
|
+
const source = readFileSync16(filePath, "utf-8");
|
|
62336
63303
|
const result = await compile2(source, {
|
|
62337
63304
|
filePath,
|
|
62338
63305
|
optimize: false,
|
|
@@ -62343,6 +63310,42 @@ async function checkCommand() {
|
|
|
62343
63310
|
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
62344
63311
|
const warnings = diagnostics.filter((d) => d.severity === "warning");
|
|
62345
63312
|
const info = diagnostics.filter((d) => d.severity === "info");
|
|
63313
|
+
const checkSource = readFileSync16(filePath, "utf-8");
|
|
63314
|
+
const usedTags = /* @__PURE__ */ new Set();
|
|
63315
|
+
const collectUsed = (n) => {
|
|
63316
|
+
if (!n) return;
|
|
63317
|
+
if (n.type === "Element" || n.type === "Component") {
|
|
63318
|
+
const t2 = n.tag ?? n.name;
|
|
63319
|
+
if (typeof t2 === "string" && t2[0] === t2[0].toUpperCase()) usedTags.add(t2);
|
|
63320
|
+
}
|
|
63321
|
+
for (const c of n.children ?? []) collectUsed(c);
|
|
63322
|
+
if (n.type === "If" || n.type === "For" || n.type === "While") for (const c of n.body ?? []) collectUsed(c);
|
|
63323
|
+
};
|
|
63324
|
+
if (result.ast) {
|
|
63325
|
+
for (const n of result.ast.body ?? []) collectUsed(n);
|
|
63326
|
+
}
|
|
63327
|
+
const SAFE_TAGS = /* @__PURE__ */ new Set(["Link", "Suspense", "RouterLink", "Image", "optImage"]);
|
|
63328
|
+
const importedNames = /* @__PURE__ */ new Set();
|
|
63329
|
+
for (const im of checkSource.matchAll(/import\s+([A-Za-z_]\w*)\s+from/g)) importedNames.add(im[1]);
|
|
63330
|
+
for (const im of checkSource.matchAll(/import\s*\{([^}]+)\}/g)) {
|
|
63331
|
+
for (const part of im[1].split(",")) {
|
|
63332
|
+
const nm = part.split(" as ").pop().trim();
|
|
63333
|
+
if (nm) importedNames.add(nm);
|
|
63334
|
+
}
|
|
63335
|
+
}
|
|
63336
|
+
const unknownComponents = [];
|
|
63337
|
+
for (const tag of usedTags) {
|
|
63338
|
+
if (SAFE_TAGS.has(tag) || importedNames.has(tag)) continue;
|
|
63339
|
+
if (findComponentUp(dirname8(filePath), tag)) continue;
|
|
63340
|
+
unknownComponents.push(tag);
|
|
63341
|
+
}
|
|
63342
|
+
if (unknownComponents.length > 0) {
|
|
63343
|
+
console.debug(` ? ${relativePath} -- ${unknownComponents.length} unknown component(s)`);
|
|
63344
|
+
for (const t2 of unknownComponents) {
|
|
63345
|
+
console.debug(` ? TW098 unknown component <${t2}> -- no import resolves it and no components/${t2}.tw exists`);
|
|
63346
|
+
}
|
|
63347
|
+
totalWarnings += unknownComponents.length;
|
|
63348
|
+
}
|
|
62346
63349
|
totalErrors += errors.length;
|
|
62347
63350
|
totalWarnings += warnings.length;
|
|
62348
63351
|
totalInfo += info.length;
|
|
@@ -62365,7 +63368,7 @@ async function checkCommand() {
|
|
|
62365
63368
|
console.debug(" Compiler not found, running basic checks...\n");
|
|
62366
63369
|
for (const filePath of twFiles) {
|
|
62367
63370
|
const relativePath = filePath.replace(rootDir + "/", "");
|
|
62368
|
-
const source =
|
|
63371
|
+
const source = readFileSync16(filePath, "utf-8");
|
|
62369
63372
|
const maskedSource = maskSourceStringsAndComments(source);
|
|
62370
63373
|
const openBraces = (maskedSource.match(/\{/g) || []).length;
|
|
62371
63374
|
const closeBraces = (maskedSource.match(/\}/g) || []).length;
|
|
@@ -62379,13 +63382,41 @@ async function checkCommand() {
|
|
|
62379
63382
|
}
|
|
62380
63383
|
console.log(`
|
|
62381
63384
|
Summary: ${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info`);
|
|
63385
|
+
const argv = process.argv.slice(2);
|
|
63386
|
+
let maxWarnings = null;
|
|
63387
|
+
if (argv.includes("--strict")) maxWarnings = 0;
|
|
63388
|
+
const mwIdx = argv.indexOf("--max-warnings");
|
|
63389
|
+
if (mwIdx !== -1 && argv[mwIdx + 1] !== void 0 && /^\d+$/.test(argv[mwIdx + 1])) {
|
|
63390
|
+
maxWarnings = Number(argv[mwIdx + 1]);
|
|
63391
|
+
} else {
|
|
63392
|
+
const eq = argv.find((a) => a.startsWith("--max-warnings="));
|
|
63393
|
+
if (eq && /^\d+$/.test(eq.split("=")[1])) maxWarnings = Number(eq.split("=")[1]);
|
|
63394
|
+
}
|
|
62382
63395
|
if (totalErrors > 0) {
|
|
62383
63396
|
console.log(" Status: FAILED\n");
|
|
62384
63397
|
process.exit(1);
|
|
63398
|
+
} else if (maxWarnings !== null && totalWarnings > maxWarnings) {
|
|
63399
|
+
console.log(` Status: FAILED -- ${totalWarnings} warnings exceed --max-warnings=${maxWarnings}
|
|
63400
|
+
`);
|
|
63401
|
+
process.exit(1);
|
|
62385
63402
|
} else {
|
|
62386
63403
|
console.log(" Status: PASSED\n");
|
|
62387
63404
|
}
|
|
62388
63405
|
}
|
|
63406
|
+
function findComponentUp(dir, name) {
|
|
63407
|
+
let cur = dir;
|
|
63408
|
+
for (let i = 0; i < 12 && cur && cur !== "/"; i++) {
|
|
63409
|
+
try {
|
|
63410
|
+
const candidate = join28(cur, "components", name + ".tw");
|
|
63411
|
+
if (existsSync23(candidate)) return true;
|
|
63412
|
+
} catch {
|
|
63413
|
+
}
|
|
63414
|
+
const parent = dirname8(cur);
|
|
63415
|
+
if (parent === cur) break;
|
|
63416
|
+
cur = parent;
|
|
63417
|
+
}
|
|
63418
|
+
return false;
|
|
63419
|
+
}
|
|
62389
63420
|
function collectFiles2(dir, ext, results) {
|
|
62390
63421
|
const entries = readdirSync10(dir);
|
|
62391
63422
|
for (const entry of entries) {
|
|
@@ -62402,10 +63433,10 @@ function collectFiles2(dir, ext, results) {
|
|
|
62402
63433
|
// tw/commands/plugin.ts
|
|
62403
63434
|
init_tw();
|
|
62404
63435
|
import { join as join29 } from "node:path";
|
|
62405
|
-
import { existsSync as existsSync24, readFileSync as
|
|
63436
|
+
import { existsSync as existsSync24, readFileSync as readFileSync17, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, readdirSync as readdirSync11 } from "node:fs";
|
|
62406
63437
|
function readConfigPlugins(cfgPath) {
|
|
62407
63438
|
if (!existsSync24(cfgPath)) return [];
|
|
62408
|
-
const src = stripCommentsStringAware(
|
|
63439
|
+
const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
|
|
62409
63440
|
const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
|
|
62410
63441
|
if (!m) return [];
|
|
62411
63442
|
const out = [];
|
|
@@ -62424,7 +63455,7 @@ function addConfigPlugin(cfgPath, name) {
|
|
|
62424
63455
|
`);
|
|
62425
63456
|
return true;
|
|
62426
63457
|
}
|
|
62427
|
-
const src = stripCommentsStringAware(
|
|
63458
|
+
const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
|
|
62428
63459
|
if (new RegExp(`["'\`]${name}["'\`]`).test(src)) return false;
|
|
62429
63460
|
const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
|
|
62430
63461
|
if (m) {
|
|
@@ -62441,7 +63472,7 @@ function addConfigPlugin(cfgPath, name) {
|
|
|
62441
63472
|
}
|
|
62442
63473
|
function removeConfigPlugin(cfgPath, name) {
|
|
62443
63474
|
if (!existsSync24(cfgPath)) return false;
|
|
62444
|
-
const src = stripCommentsStringAware(
|
|
63475
|
+
const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
|
|
62445
63476
|
const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
|
|
62446
63477
|
if (!m) return false;
|
|
62447
63478
|
const kept = m[1].split(",").map((e) => e.trim()).filter((e) => e && e !== `"${name}"` && e !== `'${name}'` && e !== "`" + name + "`");
|
|
@@ -62645,13 +63676,13 @@ async function lspCommand() {
|
|
|
62645
63676
|
}
|
|
62646
63677
|
|
|
62647
63678
|
// tw/help.ts
|
|
62648
|
-
import { readFileSync as
|
|
63679
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
62649
63680
|
import { join as join31 } from "node:path";
|
|
62650
63681
|
function cliVersion() {
|
|
62651
63682
|
try {
|
|
62652
63683
|
const bd = globalThis.__TW_BUNDLE_DIR;
|
|
62653
63684
|
const pj = bd ? join31(bd, "..", "package.json") : new URL("../../package.json", import.meta.url).pathname;
|
|
62654
|
-
return JSON.parse(
|
|
63685
|
+
return JSON.parse(readFileSync18(pj, "utf-8")).version ?? "0.0.0";
|
|
62655
63686
|
} catch {
|
|
62656
63687
|
return "0.0.0";
|
|
62657
63688
|
}
|
|
@@ -62724,13 +63755,13 @@ var helpText = `
|
|
|
62724
63755
|
|
|
62725
63756
|
// tw/commands/index.ts
|
|
62726
63757
|
await init_build();
|
|
62727
|
-
import { readFileSync as
|
|
63758
|
+
import { readFileSync as readFileSync19 } from "node:fs";
|
|
62728
63759
|
import { join as join32 } from "node:path";
|
|
62729
63760
|
var VERSION = (() => {
|
|
62730
63761
|
try {
|
|
62731
63762
|
const bd = globalThis.__TW_BUNDLE_DIR;
|
|
62732
63763
|
const pj = bd ? join32(bd, "..", "package.json") : new URL("../../package.json", import.meta.url).pathname;
|
|
62733
|
-
return JSON.parse(
|
|
63764
|
+
return JSON.parse(readFileSync19(pj, "utf-8")).version ?? "0.0.0";
|
|
62734
63765
|
} catch {
|
|
62735
63766
|
return "0.0.0";
|
|
62736
63767
|
}
|