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/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
- if (condToken) {
15013
- condition = condToken.value;
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.7";
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|mixin|include|extend|use|forward|function|each|if)\b/g;
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 and & references only");
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[12]/.test(e.message)) throw e;
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[12]/.test(e.message)) throw e;
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
- childrenHTML = comp.children.map((c) => generateNode(c, ctx)).join("");
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
- ctx.stateVars[decl.name] = decl.value;
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
- var BATCH_MS, HISTORY_LIMIT, SignalHub, hub;
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
- if (this.history.length > HISTORY_LIMIT) this.history.shift();
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
- for (const headMatch of bodyHTML.matchAll(/<head[^>]*>([\s\S]*?)<\/head>/gi)) {
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) layoutTitle = tMatch[1].trim().replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
42020
- const kids = headInner.replace(/<title>[^<]*<\/title>/gi, "").replace(/<meta[^>]+charset[^>]*>/gi, "").replace(/<meta[^>]+viewport[^>]*>/gi, "").trim();
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[^>]*>[\s\S]*?<\/head>/gi, "");
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
- if (ctx.error) {
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
- if (ctx.loading) {
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
- if (!/__tw_runtime\.js/.test(fullHTML)) {
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 result = compileSync(source, {
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
- const end = source.indexOf('"', start);
42262
- if (end === -1 || end === start) return null;
42263
- return source.slice(start, end).replace(/\\([{}"])/g, "$1");
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 ?? {}, "x-tw-cache": verdict };
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
- "X-XSS-Protection": "1; mode=block",
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
- xssProtection: "1; mode=block",
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 { port, host } = this.options;
54303
- this.server = (typeof Bun !== "undefined" ? Bun : null)?.serve({
54304
- port,
54305
- hostname: host,
54306
- fetch: async (request) => {
54307
- const res = await this.handleRequest(request);
54308
- if (this.securityHeaders) {
54309
- try {
54310
- return this.securityHeaders.apply(res);
54311
- } catch {
54312
- return res;
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
- return res;
54316
- },
54317
- error: (err3) => {
54318
- console.error("Server error:", err3);
54319
- return new Response("Internal Server Error", { status: 500 });
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 hub2 = getSignalHub();
54533
- const names = (url.searchParams.get("s") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
54534
- const since = Number(url.searchParams.get("since") ?? "0") || 0;
54535
- const declared = /* @__PURE__ */ new Map();
54536
- for (const n of names) declared.set(n, "public");
54537
- let session;
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
- return new Response(stream, {
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
- const pathname = normalizePathname(url.pathname);
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
- parsedBody = rawBody2 ? JSON.parse(rawBody2) : {};
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
- query2 = Object.fromEntries(u.searchParams.entries());
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 (!result || typeof result !== "object") {
55382
- console.error("[tw] route handler returned no result (" + filePath + ")");
55383
- return { status: 500, json: { ok: false, error: "Internal Server Error" } };
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) paramSets = arr.map((x) => {
55761
- const o = {};
55762
- for (const [k, v] of Object.entries(x)) o[k] = String(v);
55763
- return o;
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
- console.error(" \x1B[31m\u2717 " + pageFile.replace(rootDir + "/", "") + ": unknown component" + (unknown.length > 1 ? "s" : "") + " <" + unknown.join(">, <") + "> -- no components/" + unknown[0] + ".tw found (create the file or fix the import)\x1B[0m");
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 runtimeTag = `<script defer src="/__tw_runtime.js"></script>`;
55936
- html = html.replace("</body>", " " + clientTag + runtimeTag + "\n</body>");
55937
- needsRuntime = true;
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
- console.log(" \x1B[32mOK\x1B[0m /" + routeName + " [api]");
56011
- apiCount++;
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
- outputDirectory: ".tw",
60195
- installCommand: "bun install",
60196
- framework: null
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
- const server = Bun.serve({
61823
- port,
61824
- hostname: host,
61825
- async fetch(req) {
61826
- const reqUrl = new URL(req.url);
61827
- for (const r of devRedirects) {
61828
- if (!r?.from || !r?.to) continue;
61829
- if (devPathMatch(r.from, reqUrl.pathname)) {
61830
- const status = [301, 302, 307, 308].includes(r.status) ? r.status : 301;
61831
- let loc = r.to.startsWith("http") ? r.to : new URL(r.to, req.url).toString();
61832
- if (reqUrl.search && !loc.includes("?")) loc += reqUrl.search;
61833
- return new Response(null, { status, headers: { Location: loc } });
61834
- }
61835
- }
61836
- const mwHeaders = await runUserMiddleware(req);
61837
- if (mwHeaders instanceof Response) return mwHeaders;
61838
- const u = new URL(req.url);
61839
- if (u.pathname === "/_tw/img" || u.pathname.startsWith("/_tw/img/")) {
61840
- const imgRes = await devImageHandler?.handle(req);
61841
- if (imgRes) return imgRes;
61842
- }
61843
- const response = await devFetchInner(req);
61844
- if (mwHeaders && typeof mwHeaders === "object") {
61845
- for (const key of Object.keys(mwHeaders)) {
61846
- try {
61847
- if (!response.headers.has(key)) response.headers.set(key, mwHeaders[key]);
61848
- } catch {
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
- const u2 = new URL(req.url);
61853
- for (const h of devCfgHeaders) {
61854
- if (h?.source && h?.headers && devPathMatch(h.source, u2.pathname)) {
61855
- for (const key of Object.keys(h.headers)) {
61856
- try {
61857
- if (!response.headers.has(key)) response.headers.set(key, String(h.headers[key]).replace(/[\r\n]/g, ""));
61858
- } catch {
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
- if (devSecurityHeaders2) {
61864
- try {
61865
- return devSecurityHeaders2.apply(response);
61866
- } catch {
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: readFileSync19 } = await import("node:fs");
61899
- for (const line of readFileSync19(envPath, "utf-8").split("\n")) {
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
- ...typeof config?.compression === "boolean" ? { compression: config.compression } : {},
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 readFileSync15, readdirSync as readdirSync10, statSync as statSync10 } from "node:fs";
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 = readFileSync15(filePath, "utf-8");
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 = readFileSync15(filePath, "utf-8");
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 readFileSync16, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, readdirSync as readdirSync11 } from "node:fs";
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(readFileSync16(cfgPath, "utf-8"));
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(readFileSync16(cfgPath, "utf-8"));
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(readFileSync16(cfgPath, "utf-8"));
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 readFileSync17 } from "node:fs";
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(readFileSync17(pj, "utf-8")).version ?? "0.0.0";
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 readFileSync18 } from "node:fs";
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(readFileSync18(pj, "utf-8")).version ?? "0.0.0";
63764
+ return JSON.parse(readFileSync19(pj, "utf-8")).version ?? "0.0.0";
62734
63765
  } catch {
62735
63766
  return "0.0.0";
62736
63767
  }