tw-framework 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.8";
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'; 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,10 @@ 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 = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'";
48958
49749
  DEFAULTS2 = {
48959
49750
  hstsMaxAge: 31536e3,
48960
49751
  // 1 year
@@ -48963,7 +49754,9 @@ var init_security_headers = __esm({
48963
49754
  frameOptions: "SAMEORIGIN",
48964
49755
  contentTypeOptions: true,
48965
49756
  referrerPolicy: "strict-origin-when-cross-origin",
48966
- xssProtection: "1; mode=block",
49757
+ // v1.0.8 round 5 (BUG 48): x-xss-protection deprecated since 2019 --
49758
+ // OFF by default now; opt back in with security.xssProtection.
49759
+ xssProtection: void 0,
48967
49760
  removePoweredBy: true,
48968
49761
  coep: "unsafe-none",
48969
49762
  coop: "same-origin",
@@ -49019,6 +49812,9 @@ var init_security_headers = __esm({
49019
49812
  if (this.config.serverHeader) {
49020
49813
  headers["Server"] = this.config.serverHeader;
49021
49814
  }
49815
+ if (this.config.csp !== false && !headers["Content-Security-Policy"]) {
49816
+ headers["Content-Security-Policy"] = typeof this.config.csp === "string" ? this.config.csp : BASELINE_CSP;
49817
+ }
49022
49818
  return headers;
49023
49819
  }
49024
49820
  /** Returns the security headers object. */
@@ -53229,161 +54025,6 @@ var init_ssr = __esm({
53229
54025
  }
53230
54026
  });
53231
54027
 
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
54028
  // ../../packages/server/tw/edge.ts
53388
54029
  function createEdgeHandler(config) {
53389
54030
  const cache = /* @__PURE__ */ new Map();
@@ -54102,8 +54743,11 @@ var init_tw6 = __esm({
54102
54743
  init_tw2();
54103
54744
  init_websocket_manager();
54104
54745
  init_routing2();
54746
+ init_twm_rules();
54105
54747
  init_router();
54106
54748
  init_middleware2();
54749
+ init_compression();
54750
+ init_signal_stream();
54107
54751
  init_static();
54108
54752
  init_tw5();
54109
54753
  init_ssr();
@@ -54152,6 +54796,7 @@ var init_tw6 = __esm({
54152
54796
  }
54153
54797
  ssr = null;
54154
54798
  server = null;
54799
+ compressionMode = "off";
54155
54800
  isRunning = false;
54156
54801
  pluginManager = null;
54157
54802
  imageHandler = null;
@@ -54168,6 +54813,40 @@ var init_tw6 = __esm({
54168
54813
  const entries = Object.entries(cfgIn.redirects);
54169
54814
  cfgIn.redirects = entries.map(([from, to]) => typeof to === "string" ? { from, to } : { from, ...to });
54170
54815
  }
54816
+ if (Array.isArray(cfgIn.redirects)) {
54817
+ const REDIRECT_KNOWN = /* @__PURE__ */ new Set(["from", "to", "status", "source", "destination", "permanent"]);
54818
+ let redirectDropped = false;
54819
+ const normalizedRedirects = cfgIn.redirects.map((r) => {
54820
+ if (!r || typeof r !== "object") return r;
54821
+ let touched = false;
54822
+ const mapped = { ...r };
54823
+ if (mapped.from === void 0 && mapped.source !== void 0) {
54824
+ mapped.from = mapped.source;
54825
+ touched = true;
54826
+ }
54827
+ if (mapped.to === void 0 && mapped.destination !== void 0) {
54828
+ mapped.to = mapped.destination;
54829
+ touched = true;
54830
+ }
54831
+ if (mapped.status === void 0 && mapped.permanent !== void 0) {
54832
+ mapped.status = mapped.permanent ? 301 : 302;
54833
+ touched = true;
54834
+ }
54835
+ const unknown = Object.keys(mapped).filter((k) => !REDIRECT_KNOWN.has(k));
54836
+ if (unknown.length > 0) {
54837
+ console.warn("[tw] redirects: unrecognized field" + (unknown.length > 1 ? "s" : "") + " " + unknown.join(", ") + " in entry { from: " + JSON.stringify(mapped.from) + " } -- accepted fields: from, to, status");
54838
+ }
54839
+ if (typeof mapped.from !== "string" || typeof mapped.to !== "string") {
54840
+ console.warn("[tw] redirects: entry missing from/to after normalization -- skipped (" + JSON.stringify(r).slice(0, 80) + ")");
54841
+ redirectDropped = true;
54842
+ return null;
54843
+ }
54844
+ return touched ? mapped : r;
54845
+ }).filter((x) => x !== null || (redirectDropped = true));
54846
+ if (redirectDropped || normalizedRedirects.some((x, i) => x !== cfgIn.redirects[i])) {
54847
+ cfgIn.redirects = normalizedRedirects.filter(Boolean);
54848
+ }
54849
+ }
54171
54850
  if (cfgIn.headers && !Array.isArray(cfgIn.headers)) {
54172
54851
  const entries = Object.entries(cfgIn.headers);
54173
54852
  cfgIn.headers = entries.map(([source, headers]) => ({ source, headers }));
@@ -54185,6 +54864,12 @@ var init_tw6 = __esm({
54185
54864
  }
54186
54865
  }
54187
54866
  this.pluginManager = this.options.plugins ?? null;
54867
+ {
54868
+ const raw = this.options.compression ?? true;
54869
+ if (raw === false || raw === "none" || raw === "off" || raw === "false") this.compressionMode = "off";
54870
+ else if (raw === "gzip") this.compressionMode = "gzip";
54871
+ else this.compressionMode = "brotli";
54872
+ }
54188
54873
  try {
54189
54874
  this.imageHandler = createImageHandler({
54190
54875
  rootDir: this.options.rootDir,
@@ -54294,31 +54979,72 @@ var init_tw6 = __esm({
54294
54979
  if (existsSync13(middlewarePath)) {
54295
54980
  this.middlewarePath = middlewarePath;
54296
54981
  console.log(" Middleware: middleware.twm loaded");
54982
+ try {
54983
+ const src = readFileSync9(middlewarePath, "utf-8");
54984
+ const rules = parseRules(src, "middleware.twm");
54985
+ for (const r of rules) {
54986
+ const conds = [];
54987
+ if (r.userAgent) conds.push("user_agent");
54988
+ if (r.path) conds.push("path");
54989
+ if (r.auth) conds.push("auth");
54990
+ if (r.rateLimit) conds.push("rate_limit");
54991
+ if (r.origin) conds.push("origin");
54992
+ if (r.methods && r.methods.length > 0) conds.push("methods");
54993
+ const label = conds.length > 0 ? conds.join(" + ") : "no conditions -> every matching request gets this response";
54994
+ console.log(` \u2022 "${r.name}" [${label}]`);
54995
+ }
54996
+ } catch {
54997
+ }
54297
54998
  }
54298
54999
  return this;
54299
55000
  }
54300
55001
  async start() {
54301
55002
  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;
55003
+ const host = this.options.host ?? "0.0.0.0";
55004
+ const wanted = this.options.port ?? 8e3;
55005
+ let port = wanted;
55006
+ let bound = null;
55007
+ let lastErr = null;
55008
+ for (let p = wanted; p < wanted + 10; p++) {
55009
+ try {
55010
+ bound = (typeof Bun !== "undefined" ? Bun : null)?.serve({
55011
+ port: p,
55012
+ hostname: host,
55013
+ fetch: async (request) => {
55014
+ const res = await this.handleRequest(request);
55015
+ let withHeaders = res;
55016
+ if (this.securityHeaders) {
55017
+ try {
55018
+ withHeaders = this.securityHeaders.apply(res);
55019
+ } catch {
55020
+ withHeaders = res;
55021
+ }
55022
+ }
55023
+ return this.compressResponse(withHeaders, request);
55024
+ },
55025
+ error: (err3) => {
55026
+ console.error("Server error:", err3);
55027
+ return new Response("Internal Server Error", { status: 500 });
54313
55028
  }
54314
- }
54315
- return res;
54316
- },
54317
- error: (err3) => {
54318
- console.error("Server error:", err3);
54319
- return new Response("Internal Server Error", { status: 500 });
55029
+ });
55030
+ port = p;
55031
+ break;
55032
+ } catch (e) {
55033
+ lastErr = e;
55034
+ const msg = String(e?.message ?? e);
55035
+ if (!/address already in use|EADDRINUSE|Failed to start server/i.test(msg)) throw e;
55036
+ if (p < wanted + 9) console.log(` Port ${p} busy \u2014 trying ${p + 1}...`);
54320
55037
  }
54321
- });
55038
+ }
55039
+ if (!bound) {
55040
+ console.error(`
55041
+ ERROR: could not bind ${host} on ports ${wanted}\u2013${wanted + 9}.`);
55042
+ console.error(` ${lastErr?.message ?? lastErr}
55043
+ `);
55044
+ throw lastErr ?? new Error("could not start server");
55045
+ }
55046
+ this.server = bound;
55047
+ this.options.port = port;
54322
55048
  this.isRunning = true;
54323
55049
  console.log(`
54324
55050
  TW Server running at http://${host}:${port}
@@ -54332,6 +55058,10 @@ var init_tw6 = __esm({
54332
55058
  // finishes ongoing requests, then closes
54333
55059
  async gracefulShutdown(timeout = 5e3) {
54334
55060
  this.isRunning = false;
55061
+ try {
55062
+ getSignalHub().closeAll();
55063
+ } catch {
55064
+ }
54335
55065
  if (this.server) {
54336
55066
  this.server.stop?.(false);
54337
55067
  const start = Date.now();
@@ -54357,6 +55087,35 @@ var init_tw6 = __esm({
54357
55087
  * - return { headers: {...} } -> headers merged into the final response
54358
55088
  * - return null/undefined -> continue
54359
55089
  */
55090
+ /** BUG 7 (v1.0.8): real response compression. `compression` in
55091
+ * tw.config.ts was silently ignored -- the middleware was a no-op and
55092
+ * nothing on the response path ever set Content-Encoding. */
55093
+ async compressResponse(res, request) {
55094
+ if (this.compressionMode === "off") return res;
55095
+ try {
55096
+ if (res.headers.get("content-encoding")) return res;
55097
+ const ct = res.headers.get("content-type") || "";
55098
+ if (ct.includes("text/event-stream")) return res;
55099
+ if (res.__twRawStream) return res;
55100
+ const buf = new Uint8Array(await res.arrayBuffer());
55101
+ if (!isLargeEnough(buf.length)) return new Response(buf, { status: res.status, headers: res.headers });
55102
+ const accept = request.headers.get("accept-encoding") || "";
55103
+ let enc = negotiateEncoding(accept);
55104
+ if (this.compressionMode === "gzip" && enc === "brotli") enc = "gzip";
55105
+ if (!enc) return new Response(buf, { status: res.status, headers: res.headers });
55106
+ const out = await compress(buf, enc);
55107
+ if (out.length >= buf.length) return new Response(buf, { status: res.status, headers: res.headers });
55108
+ const h = new Headers(res.headers);
55109
+ h.set("Content-Encoding", enc === "brotli" ? "br" : enc);
55110
+ h.set("Vary", "Accept-Encoding");
55111
+ h.delete("Content-Length");
55112
+ return new Response(out, { status: res.status, headers: h });
55113
+ } catch {
55114
+ return res;
55115
+ }
55116
+ }
55117
+ /** v1.0.8 round 5 (BUG 39): public now -- serverless adapters (Vercel
55118
+ * functions, edge runtimes) drive the server through this entry. */
54360
55119
  async handleRequest(request) {
54361
55120
  try {
54362
55121
  const u = new URL(request.url);
@@ -54529,69 +55288,12 @@ var init_tw6 = __esm({
54529
55288
  * `data: {"v":1,"seq":N,"updates":[...]}` frames.
54530
55289
  */
54531
55290
  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
- });
55291
+ const cfg = this.options.config?.signalStream;
55292
+ return createSignalStream(request, url, cfg ? { maxClients: cfg.maxClients, maxHistory: cfg.maxHistory } : void 0);
55293
+ }
55294
+ async handleClientSignalWrite(request) {
55295
+ const cfg = this.options.config?.signalStream;
55296
+ return writeSignalFromClient(request, { clientWrites: cfg?.clientWrites });
54595
55297
  }
54596
55298
  /**
54597
55299
  * PPR hole fill: render the page fresh and return only the requested
@@ -54665,15 +55367,33 @@ var init_tw6 = __esm({
54665
55367
  controller.close();
54666
55368
  }
54667
55369
  });
54668
- return new Response(stream, {
55370
+ const streamRes = new Response(stream, {
54669
55371
  status: 200,
54670
55372
  headers: { "Content-Type": "text/html; charset=utf-8" }
54671
55373
  });
55374
+ streamRes.__twRawStream = true;
55375
+ return streamRes;
54672
55376
  }
54673
55377
  async handleRequestInner(request) {
54674
55378
  const url = new URL(request.url);
54675
55379
  const method = request.method;
54676
- const pathname = normalizePathname(url.pathname);
55380
+ let rawPath = url.pathname;
55381
+ try {
55382
+ const decoded = decodeURIComponent(rawPath);
55383
+ if (decoded !== rawPath) rawPath = decoded;
55384
+ } catch {
55385
+ }
55386
+ let canonical = normalizePathname(rawPath);
55387
+ if (canonical.length > 1 && canonical.endsWith("/")) {
55388
+ canonical = canonical.slice(0, -1);
55389
+ }
55390
+ if (canonical !== rawPath) {
55391
+ return new Response(null, {
55392
+ status: 308,
55393
+ headers: { Location: encodeURI(canonical) + url.search }
55394
+ });
55395
+ }
55396
+ const pathname = canonical;
54677
55397
  if (pathname === "/_tw/img" || pathname.startsWith("/_tw/img/")) {
54678
55398
  const imgRes = await this.imageHandler.handle(request);
54679
55399
  if (imgRes) return imgRes;
@@ -54681,6 +55401,9 @@ var init_tw6 = __esm({
54681
55401
  if (pathname === "/_tw/stream") {
54682
55402
  return this.handleSignalStream(request, url);
54683
55403
  }
55404
+ if (pathname === "/_tw/signal" && method === "POST") {
55405
+ return this.handleClientSignalWrite(request);
55406
+ }
54684
55407
  if (pathname === "/_tw/ppr") {
54685
55408
  return this.handlePPRHoleFill(url);
54686
55409
  }
@@ -54845,13 +55568,39 @@ __export(twm_loader_exports, {
54845
55568
  loadTWMModule: () => loadTWMModule,
54846
55569
  revalidateRoute: () => revalidateRoute,
54847
55570
  shouldMatchMiddleware: () => shouldMatchMiddleware,
54848
- stripCommentsSafe: () => stripCommentsSafe
55571
+ stripCommentsSafe: () => stripCommentsSafe,
55572
+ validateTWMSyntax: () => validateTWMSyntax
54849
55573
  });
54850
55574
  import { readFileSync as readFileSync10, existsSync as existsSync14 } from "node:fs";
54851
55575
  import { join as join18 } from "node:path";
54852
55576
  function clearTWMCache() {
54853
55577
  moduleCache.clear();
54854
55578
  }
55579
+ function validateTWMSyntax(filePath) {
55580
+ let source;
55581
+ try {
55582
+ source = readFileSync10(filePath, "utf-8");
55583
+ } catch (err3) {
55584
+ return "cannot read: " + (err3?.message ?? err3);
55585
+ }
55586
+ if (isRuleDsl(source)) {
55587
+ try {
55588
+ parseRules(source, filePath);
55589
+ return null;
55590
+ } catch (err3) {
55591
+ return "rule DSL: " + (err3?.message ?? String(err3));
55592
+ }
55593
+ }
55594
+ try {
55595
+ const imports = extractImports(source);
55596
+ const jsCode = compileTwm(source);
55597
+ const codeWithoutImports = stripImports(jsCode);
55598
+ new Function("__tw_unused", codeWithoutImports);
55599
+ return null;
55600
+ } catch (err3) {
55601
+ return (err3?.message ?? String(err3)).replace(/\n/g, " ").slice(0, 200);
55602
+ }
55603
+ }
54855
55604
  function stripCommentsSafe(src) {
54856
55605
  let out = "";
54857
55606
  let i = 0;
@@ -55089,7 +55838,7 @@ async function loadTWMModule(filePath, rootDir) {
55089
55838
  console.error("[tw] " + e.message + " (" + filePath + ":" + cm[1] + ")");
55090
55839
  }
55091
55840
  }
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(", ") + " };";
55841
+ 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
55842
  const injectedKeys = Object.keys(injected);
55094
55843
  let module = {};
55095
55844
  try {
@@ -55104,6 +55853,8 @@ async function loadTWMModule(filePath, rootDir) {
55104
55853
  put: H.put || H.PUT || void 0,
55105
55854
  delete: H.deleteFn || H.DELETE || void 0,
55106
55855
  patch: H.patch || H.PATCH || void 0,
55856
+ // v1.0.8 round 4 (BUG 33): route-level OPTIONS handlers
55857
+ options: H.options || H.OPTIONS || void 0,
55107
55858
  middleware: H.middleware || void 0,
55108
55859
  generate: H.generate || void 0,
55109
55860
  config: H.config || void 0,
@@ -55239,9 +55990,22 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
55239
55990
  if (rawBody2.length > MAX_BODY_BYTES) {
55240
55991
  return { status: 413, json: { ok: false, error: "Payload too large" } };
55241
55992
  }
55242
- parsedBody = rawBody2 ? JSON.parse(rawBody2) : {};
55993
+ if (!rawBody2) {
55994
+ parsedBody = {};
55995
+ } else if (__ct.includes("application/x-www-form-urlencoded")) {
55996
+ parsedBody = Object.fromEntries(new URLSearchParams(rawBody2).entries());
55997
+ } else if (__ct.startsWith("text/")) {
55998
+ parsedBody = rawBody2;
55999
+ } else if (__ct.includes("json") || __ct === "") {
56000
+ try {
56001
+ parsedBody = JSON.parse(rawBody2);
56002
+ } catch (e) {
56003
+ return { status: 400, json: { ok: false, error: "Invalid JSON body: " + (e?.message ?? "parse error") } };
56004
+ }
56005
+ } else {
56006
+ return { status: 415, json: { ok: false, error: "Unsupported Content-Type: " + (__ct || "(none)") } };
56007
+ }
55243
56008
  } catch {
55244
- parsedBody = {};
55245
56009
  }
55246
56010
  } else if (request?.body && typeof request.body === "object") {
55247
56011
  parsedBody = request.body;
@@ -55249,7 +56013,18 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
55249
56013
  let query2 = {};
55250
56014
  try {
55251
56015
  const u = new URL(request.url);
55252
- query2 = Object.fromEntries(u.searchParams.entries());
56016
+ for (const [rawKey, value] of u.searchParams.entries()) {
56017
+ const isPhpArray = rawKey.endsWith("[]");
56018
+ const key = isPhpArray ? rawKey.slice(0, -2) : rawKey;
56019
+ const existing = query2[key];
56020
+ if (existing === void 0) {
56021
+ query2[key] = isPhpArray ? [value] : value;
56022
+ } else if (Array.isArray(existing)) {
56023
+ existing.push(value);
56024
+ } else {
56025
+ query2[key] = [existing, value];
56026
+ }
56027
+ }
55253
56028
  } catch {
55254
56029
  }
55255
56030
  const headersObj = typeof request.headers?.entries === "function" ? Object.fromEntries(request.headers.entries()) : request.headers ?? {};
@@ -55378,9 +56153,17 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
55378
56153
  console.error("[tw] route handler error (" + filePath + "):", err3?.message ?? err3);
55379
56154
  return { status: 500, json: { ok: false, error: "Internal Server Error" } };
55380
56155
  }
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" } };
56156
+ if (typeof result === "string") {
56157
+ result = { status: 200, text: result };
56158
+ } else if (!result || typeof result !== "object") {
56159
+ console.error("[tw] route handler returned " + JSON.stringify(result) + " (" + filePath + ")");
56160
+ return {
56161
+ status: 500,
56162
+ json: {
56163
+ ok: false,
56164
+ error: "Unsupported handler return shape: " + (result === null ? "null" : typeof result) + ". Return { status, json|text|html } (or a plain string for text/plain)."
56165
+ }
56166
+ };
55384
56167
  }
55385
56168
  const finalResult = {
55386
56169
  status: typeof result.status === "number" ? result.status : 200,
@@ -55735,11 +56518,18 @@ async function buildCommand() {
55735
56518
  } catch {
55736
56519
  }
55737
56520
  let pageCount = 0;
56521
+ let requestRenderedCount = 0;
55738
56522
  let apiCount = 0;
55739
56523
  let errorCount = 0;
55740
56524
  const isrRoutes = /* @__PURE__ */ new Map();
55741
56525
  const pageFiles = [];
55742
56526
  collectFiles(homeDir, "page.tw", pageFiles);
56527
+ for (const pf of pageFiles) {
56528
+ if (existsSync16(join20(dirname6(pf), "index.tw"))) {
56529
+ console.log(" \x1B[33m!\x1B[0m " + dirname6(pf).replace(homeDir, "").replace(/^\//, "") + "/ has BOTH page.tw and index.tw");
56530
+ console.log(" index.tw wins this route -- delete one of them");
56531
+ }
56532
+ }
55743
56533
  begin("compile", pageFiles.length);
55744
56534
  const htmlOutputs = [];
55745
56535
  _compilerMod.clearCssCapture?.();
@@ -55757,11 +56547,20 @@ async function buildCommand() {
55757
56547
  const pmod = await loadTWMModule2(paramsTwm, rootDir);
55758
56548
  if (typeof pmod.generate === "function") {
55759
56549
  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
- });
56550
+ if (Array.isArray(arr) && arr.length > 0) {
56551
+ const dynNames = pageFile.split("/").map((seg) => /^\[([^\]]+)\]$/.exec(seg)?.[1]).filter(Boolean);
56552
+ paramSets = arr.map((x) => {
56553
+ if (x !== null && typeof x !== "object") {
56554
+ const o2 = {};
56555
+ const key = dynNames[0] ?? "slug";
56556
+ o2[key] = String(x);
56557
+ return o2;
56558
+ }
56559
+ const o = {};
56560
+ for (const [k, v] of Object.entries(x)) o[k] = String(v);
56561
+ return o;
56562
+ });
56563
+ }
55765
56564
  }
55766
56565
  } catch (e) {
55767
56566
  console.log(" Warning: params.twm failed: " + e.message);
@@ -55812,6 +56611,7 @@ async function buildCommand() {
55812
56611
  }
55813
56612
  const hasExplicitParams = paramSets.length > 1 || Object.keys(paramSets[0] ?? {}).length > 0;
55814
56613
  if ((renderMode === "ssr" || renderMode === "stream") && !hasExplicitParams) {
56614
+ requestRenderedCount++;
55815
56615
  console.log(" \x1B[36m~\x1B[0m " + routePath + " [" + renderMode + ": rendered at request time]");
55816
56616
  try {
55817
56617
  const src = readFileSync12(pageFile, "utf-8");
@@ -55822,6 +56622,29 @@ async function buildCommand() {
55822
56622
  for (const d of errs.slice(0, 5)) console.log(" " + d.line + ":" + d.col + " " + d.message);
55823
56623
  errorCount++;
55824
56624
  }
56625
+ const warns = (r.diagnostics || []).filter((d) => d.severity !== "error");
56626
+ if (warns.length > 0) {
56627
+ console.log(" \x1B[33m" + warns.length + " warning" + (warns.length === 1 ? "" : "s") + "\x1B[0m");
56628
+ for (const d of warns.slice(0, 3)) {
56629
+ console.log(" " + d.line + ":" + d.col + " " + d.code + " " + d.message);
56630
+ }
56631
+ if (warns.length > 3) console.log(" ... " + (warns.length - 3) + " more (tw check)");
56632
+ }
56633
+ {
56634
+ const used = /* @__PURE__ */ new Set();
56635
+ collectComponentUsages(r.ast, used);
56636
+ const BUILTIN_ALIASES = /* @__PURE__ */ new Set(["Link", "RouterLink", "Image", "optImage", "Suspense"]);
56637
+ const unknownSsr = [...used].filter((t2) => !BUILTIN_ALIASES.has(t2) && !registeredComponentNames.has(t2));
56638
+ if (unknownSsr.length > 0) {
56639
+ const importSpecs = /* @__PURE__ */ new Map();
56640
+ for (const im of src.matchAll(/import\s+([A-Za-z_]\w*)\s+from\s+["']([^"']+)["']/g)) {
56641
+ importSpecs.set(im[1], im[2]);
56642
+ }
56643
+ const hints = unknownSsr.map((t2) => importSpecs.has(t2) ? `<${t2}> (imported from ${importSpecs.get(t2)} -- file not found)` : `<${t2}> (no components/${t2}.tw found)`);
56644
+ console.log(" \x1B[31m\u2717 " + routePath + ": unknown component" + (unknownSsr.length > 1 ? "s" : "") + " " + hints.join(", ") + "\x1B[0m");
56645
+ errorCount++;
56646
+ }
56647
+ }
55825
56648
  } catch (e) {
55826
56649
  console.log(" \x1B[33m! " + routePath + ": " + (e && e.message ? e.message : e) + "\x1B[0m");
55827
56650
  errorCount++;
@@ -55840,7 +56663,12 @@ async function buildCommand() {
55840
56663
  const BUILTIN_ALIASES = /* @__PURE__ */ new Set(["Link", "RouterLink", "Image", "optImage", "Suspense"]);
55841
56664
  const unknown = [...used].filter((t2) => !BUILTIN_ALIASES.has(t2) && !registeredComponentNames.has(t2));
55842
56665
  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");
56666
+ const importSpecs = /* @__PURE__ */ new Map();
56667
+ for (const im of source.matchAll(/import\s+([A-Za-z_]\w*)\s+from\s+["']([^"']+)["']/g)) {
56668
+ importSpecs.set(im[1], im[2]);
56669
+ }
56670
+ const hints = unknown.map((t2) => importSpecs.has(t2) ? `<${t2}> (imported from ${importSpecs.get(t2)} -- file not found)` : `<${t2}> (no components/${t2}.tw found)`);
56671
+ 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
56672
  process.exit(1);
55845
56673
  }
55846
56674
  }
@@ -55932,9 +56760,12 @@ async function buildCommand() {
55932
56760
  if (pu) clientTag += `<script defer src="/${pu}"></script>
55933
56761
  `;
55934
56762
  }
55935
- const runtimeTag = `<script defer src="/__tw_runtime.js"></script>`;
55936
- html = html.replace("</body>", " " + clientTag + runtimeTag + "\n</body>");
55937
- needsRuntime = true;
56763
+ const needsClient = interactive || pageMods.deps.length > 0 || pageMods.own.length > 0 || renderMode === "csr" || renderMode === "ppr" || renderMode === "stream" || renderMode === "signalStream";
56764
+ if (needsClient) {
56765
+ const runtimeTag = `<script defer src="/__tw_runtime.js"></script>`;
56766
+ html = html.replace("</body>", " " + clientTag + runtimeTag + "\n</body>");
56767
+ needsRuntime = true;
56768
+ }
55938
56769
  }
55939
56770
  if (renderMode === "csr") {
55940
56771
  html = csrifyHtml(html);
@@ -55989,6 +56820,14 @@ async function buildCommand() {
55989
56820
  } else {
55990
56821
  console.log(" \x1B[32mOK\x1B[0m " + displayPath + " -> " + (outRoute === "" ? ".tw/index.html" : ".tw/" + outRoute + "/index.html"));
55991
56822
  }
56823
+ const warnings = result.diagnostics.filter((d) => d.severity !== "error");
56824
+ if (warnings.length > 0) {
56825
+ console.log(" \x1B[33m" + warnings.length + " warning" + (warnings.length === 1 ? "" : "s") + "\x1B[0m");
56826
+ for (const d of warnings.slice(0, 3)) {
56827
+ console.log(" " + d.line + ":" + d.col + " " + d.code + " " + d.message);
56828
+ }
56829
+ if (warnings.length > 3) console.log(" ... " + (warnings.length - 3) + " more (tw check)");
56830
+ }
55992
56831
  pageCount++;
55993
56832
  } catch (err3) {
55994
56833
  console.log(" \x1B[31mX " + routePath + ": " + err3.message + "\x1B[0m");
@@ -56007,8 +56846,16 @@ async function buildCommand() {
56007
56846
  const outputDir = routeName === "" ? outDir : join20(outDir, routeName);
56008
56847
  mkdirSync6(outputDir, { recursive: true });
56009
56848
  copyFileSync2(twmFile, join20(outputDir, "route.twm"));
56010
- console.log(" \x1B[32mOK\x1B[0m /" + routeName + " [api]");
56011
- apiCount++;
56849
+ const { validateTWMSyntax: validateTWMSyntax2 } = await Promise.resolve().then(() => (init_twm_loader(), twm_loader_exports));
56850
+ const syntaxError = validateTWMSyntax2(twmFile);
56851
+ if (syntaxError) {
56852
+ console.log(" \x1B[31mX\x1B[0m /" + routeName + " [api] -- .twm syntax error");
56853
+ console.log(" " + twmFile.replace(rootDir + "/", "") + ": " + syntaxError);
56854
+ errorCount++;
56855
+ } else {
56856
+ console.log(" \x1B[32mOK\x1B[0m /" + routeName + " [api]");
56857
+ apiCount++;
56858
+ }
56012
56859
  }
56013
56860
  if (existsSync16(libDir)) {
56014
56861
  copyDir2(libDir, join20(outDir, "lib"));
@@ -56087,7 +56934,7 @@ async function buildCommand() {
56087
56934
  }
56088
56935
  }
56089
56936
  console.log("\n \x1B[32mBuild complete!\x1B[0m");
56090
- console.log(" Pages: " + pageCount + " compiled");
56937
+ console.log(" Pages: " + pageCount + " compiled" + (requestRenderedCount > 0 ? " (+" + requestRenderedCount + " rendered at request time)" : ""));
56091
56938
  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
56939
  console.log(" APIs: " + apiCount + " routes");
56093
56940
  if (isrRoutes.size > 0) {
@@ -60191,15 +61038,37 @@ ${name}/
60191
61038
  async function createVercelJson(dir) {
60192
61039
  const config = {
60193
61040
  buildCommand: "npx tw build",
60194
- outputDirectory: ".tw",
60195
- installCommand: "bun install",
60196
- framework: null
61041
+ installCommand: "npm install",
61042
+ functions: {
61043
+ "api/tw-server.ts": { memory: 512 }
61044
+ },
61045
+ rewrites: [
61046
+ { source: "/(.*)", destination: "/api/tw-server" }
61047
+ ]
60197
61048
  };
60198
61049
  await writeFile(join(dir, "vercel.json"), JSON.stringify(config, null, 2));
60199
61050
  console.log(" \u2713 vercel.json");
61051
+ const fnDir = join(dir, "api");
61052
+ await mkdir(fnDir, { recursive: true });
61053
+ await writeFile(join(fnDir, "tw-server.ts"), [
61054
+ "// TW Framework -- Vercel serverless entry (v1.0.8 round 5, BUG 39).",
61055
+ "// Runs the full TW server (SSR pages + .twm API routes) as a Vercel",
61056
+ "// function. vercel.json rewrites every route here.",
61057
+ 'import { TWServer } from "tw-framework/server";',
61058
+ "",
61059
+ "const server = new TWServer({",
61060
+ " rootDir: process.cwd(),",
61061
+ "});",
61062
+ "",
61063
+ "export default async function handler(request: Request): Promise<Response> {",
61064
+ " return server.handleRequest(request);",
61065
+ "}",
61066
+ ""
61067
+ ].join("\n"));
61068
+ console.log(" \u2713 api/tw-server.ts");
60200
61069
  }
60201
61070
  async function createDefaultProject(dir, name) {
60202
- const T = (s) => s;
61071
+ const T = (s) => s.split("${name}").join(name);
60203
61072
  for (const d of [
60204
61073
  ["home"],
60205
61074
  ["home", "about"],
@@ -60222,8 +61091,6 @@ html {
60222
61091
  head {
60223
61092
  meta charset "utf-8"
60224
61093
  meta name "viewport" content "width=device-width, initial-scale=1"
60225
- meta name "description" content "{page.description}"
60226
- title "{page.title} -- ${"$"}{name}"
60227
61094
  }
60228
61095
  body {
60229
61096
  header.sitenav {
@@ -60559,6 +61426,10 @@ a:hover { td underline }
60559
61426
  // This sample blocks known bot user agents everywhere. Guards are
60560
61427
  // fail-closed: a matching request that fails the condition gets the
60561
61428
  // rule's response, and no page code ever runs.
61429
+ //
61430
+ // Note: a rule with ONLY match + response (no condition blocks) applies
61431
+ // to EVERY matching request -- e.g. a site-wide maintenance page. tw serve
61432
+ // lists each rule at startup and flags those unconditional ones.
60562
61433
 
60563
61434
  rule "block bots" {
60564
61435
  match "/**"
@@ -61522,6 +62393,7 @@ async function devCommand() {
61522
62393
  }
61523
62394
  let devRedirects = [];
61524
62395
  let devCfgHeaders = [];
62396
+ let devSigCfg = void 0;
61525
62397
  try {
61526
62398
  const cfgFile = join19(rootDir, "tw.config.ts");
61527
62399
  if (existsSync15(cfgFile)) {
@@ -61534,12 +62406,24 @@ async function devCommand() {
61534
62406
  if (cfg?.headers) {
61535
62407
  devCfgHeaders = Array.isArray(cfg.headers) ? cfg.headers : Object.entries(cfg.headers).map(([source, headers]) => ({ source, headers }));
61536
62408
  }
62409
+ if (cfg?.signalStream) devSigCfg = cfg.signalStream;
61537
62410
  }
61538
62411
  } catch {
61539
62412
  }
61540
62413
  const devPathMatch = (pattern, pathname) => pattern === pathname || pattern.endsWith("/**") && pathname.startsWith(pattern.slice(0, -2) + "/") || pattern.endsWith("/") && pathname.startsWith(pattern) || pattern === "/**" && pathname.startsWith("/");
61541
62414
  async function devFetchInner(req) {
61542
62415
  const url = new URL(req.url);
62416
+ try {
62417
+ let rawPath = url.pathname;
62418
+ const decoded = decodeURIComponent(rawPath);
62419
+ if (decoded !== rawPath) rawPath = decoded;
62420
+ let canonical = rawPath.replace(/\/{2,}/g, "/");
62421
+ if (canonical.length > 1 && canonical.endsWith("/")) canonical = canonical.slice(0, -1);
62422
+ if (canonical !== rawPath) {
62423
+ return new Response(null, { status: 308, headers: { Location: encodeURI(canonical) + url.search } });
62424
+ }
62425
+ } catch {
62426
+ }
61543
62427
  const path = url.pathname;
61544
62428
  const time = (/* @__PURE__ */ new Date()).toISOString().split("T")[1].split(".")[0];
61545
62429
  rescanComponentsIfNeeded(rootDir);
@@ -61819,56 +62703,108 @@ async function devCommand() {
61819
62703
  headers: { "Content-Type": "text/html" }
61820
62704
  });
61821
62705
  }
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
- }
62706
+ async function devHandle(req) {
62707
+ {
62708
+ const u3 = new URL(req.url);
62709
+ if (u3.pathname === "/_tw/stream") {
62710
+ const { createSignalStream: createSignalStream2 } = await Promise.resolve().then(() => (init_signal_stream(), signal_stream_exports));
62711
+ return createSignalStream2(req, u3, devSigCfg ? { maxClients: devSigCfg.maxClients, maxHistory: devSigCfg.maxHistory } : void 0);
62712
+ }
62713
+ if (u3.pathname === "/_tw/signal" && req.method === "POST") {
62714
+ const { writeSignalFromClient: writeSignalFromClient2 } = await Promise.resolve().then(() => (init_signal_stream(), signal_stream_exports));
62715
+ return writeSignalFromClient2(req, { clientWrites: devSigCfg?.clientWrites });
62716
+ }
62717
+ }
62718
+ const reqUrl = new URL(req.url);
62719
+ for (const r of devRedirects) {
62720
+ if (!r?.from || !r?.to) continue;
62721
+ if (devPathMatch(r.from, reqUrl.pathname)) {
62722
+ const status = [301, 302, 307, 308].includes(r.status) ? r.status : 301;
62723
+ let loc = r.to.startsWith("http") ? r.to : new URL(r.to, req.url).toString();
62724
+ if (reqUrl.search && !loc.includes("?")) loc += reqUrl.search;
62725
+ return new Response(null, { status, headers: { Location: loc } });
62726
+ }
62727
+ }
62728
+ const mwHeaders = await runUserMiddleware(req);
62729
+ if (mwHeaders instanceof Response) return mwHeaders;
62730
+ const u = new URL(req.url);
62731
+ if (u.pathname === "/_tw/img" || u.pathname.startsWith("/_tw/img/")) {
62732
+ const imgRes = await devImageHandler?.handle(req);
62733
+ if (imgRes) return imgRes;
62734
+ }
62735
+ const response = await devFetchInner(req);
62736
+ if (mwHeaders && typeof mwHeaders === "object") {
62737
+ for (const key of Object.keys(mwHeaders)) {
62738
+ try {
62739
+ if (!response.headers.has(key)) response.headers.set(key, mwHeaders[key]);
62740
+ } catch {
61850
62741
  }
61851
62742
  }
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
- }
62743
+ }
62744
+ const u2 = new URL(req.url);
62745
+ for (const h of devCfgHeaders) {
62746
+ if (h?.source && h?.headers && devPathMatch(h.source, u2.pathname)) {
62747
+ for (const key of Object.keys(h.headers)) {
62748
+ try {
62749
+ if (!response.headers.has(key)) response.headers.set(key, String(h.headers[key]).replace(/[\r\n]/g, ""));
62750
+ } catch {
61860
62751
  }
61861
62752
  }
61862
62753
  }
61863
- if (devSecurityHeaders2) {
61864
- try {
61865
- return devSecurityHeaders2.apply(response);
61866
- } catch {
61867
- }
62754
+ }
62755
+ if (devSecurityHeaders2) {
62756
+ try {
62757
+ return devSecurityHeaders2.apply(response);
62758
+ } catch {
61868
62759
  }
61869
- return response;
61870
62760
  }
61871
- });
62761
+ return response;
62762
+ }
62763
+ if (typeof Bun !== "undefined" && typeof Bun.serve === "function") {
62764
+ Bun.serve({ port, hostname: host, fetch: devHandle });
62765
+ } else {
62766
+ const { createServer } = await import("node:http");
62767
+ const srv = createServer(async (nreq, nres) => {
62768
+ try {
62769
+ const hostHdr = String(nreq.headers.host ?? `${host}:${port}`);
62770
+ const chunks = [];
62771
+ for await (const c of nreq) chunks.push(Buffer.from(c));
62772
+ const raw = nreq.method === "GET" || nreq.method === "HEAD" ? void 0 : Buffer.concat(chunks);
62773
+ const request = new Request(`http://${hostHdr}${nreq.url}`, {
62774
+ method: nreq.method,
62775
+ headers: nreq.headers,
62776
+ body: raw && raw.length > 0 ? raw : void 0
62777
+ });
62778
+ const resp = await devHandle(request);
62779
+ const out = Buffer.from(await resp.arrayBuffer());
62780
+ const headers = {};
62781
+ resp.headers.forEach((v, k) => {
62782
+ headers[k] = v;
62783
+ });
62784
+ nres.writeHead(resp.status, headers);
62785
+ nres.end(out);
62786
+ } catch (e) {
62787
+ nres.writeHead(500, { "Content-Type": "text/plain" });
62788
+ nres.end("Internal dev server error: " + (e?.message ?? e));
62789
+ }
62790
+ });
62791
+ await new Promise((r) => srv.listen(port, host, () => r()));
62792
+ console.log(" [tw] runtime: node (Bun not found \u2014 using node:http)");
62793
+ }
62794
+ try {
62795
+ const { watch: watch2 } = await import("node:fs");
62796
+ const seen = /* @__PURE__ */ new Set();
62797
+ watch2(rootDir, { recursive: true }, (_t, f) => {
62798
+ const name = String(f);
62799
+ if (name.includes("node_modules") || name.includes(".git") || name.split("/").includes(".tw")) return;
62800
+ if (seen.has(name)) return;
62801
+ seen.add(name);
62802
+ const t2 = setTimeout(() => seen.delete(name), 400);
62803
+ if (typeof t2?.unref === "function") t2.unref();
62804
+ console.log(` [tw] ${name} changed -- reload the browser`);
62805
+ });
62806
+ } catch {
62807
+ }
61872
62808
  console.log(" Server running! Open http://" + host + ":" + port + " in your browser.");
61873
62809
  console.log(" Press Ctrl+C to stop.\n");
61874
62810
  }
@@ -61881,6 +62817,25 @@ import { join as join22 } from "node:path";
61881
62817
  import { existsSync as existsSync18 } from "node:fs";
61882
62818
  async function serveCommand() {
61883
62819
  const args = process.argv.slice(3);
62820
+ if (args.includes("-h") || args.includes("--help")) {
62821
+ console.log(`
62822
+ tw serve \u2014 start the production server
62823
+
62824
+ Usage:
62825
+ tw serve [--port <number>] [--host <address>]
62826
+
62827
+ Options:
62828
+ --port <number> port to listen on (default: 8000; tw.config.ts
62829
+ server.port and the PORT env var also work,
62830
+ in that order of precedence)
62831
+ --host <address> bind address (default: 0.0.0.0)
62832
+ -h, --help show this help
62833
+
62834
+ Requires a build first (tw build); serves .tw/ output with SSR,
62835
+ middleware and API routes.
62836
+ `);
62837
+ process.exit(0);
62838
+ }
61884
62839
  let port = null;
61885
62840
  let host = "0.0.0.0";
61886
62841
  for (let i = 0; i < args.length; i++) {
@@ -61895,8 +62850,8 @@ async function serveCommand() {
61895
62850
  const envPath = join22(rootDir, ".env");
61896
62851
  if (existsSync18(envPath)) {
61897
62852
  try {
61898
- const { readFileSync: readFileSync19 } = await import("node:fs");
61899
- for (const line of readFileSync19(envPath, "utf-8").split("\n")) {
62853
+ const { readFileSync: readFileSync20 } = await import("node:fs");
62854
+ for (const line of readFileSync20(envPath, "utf-8").split("\n")) {
61900
62855
  const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/.exec(line);
61901
62856
  if (m && !(m[1] in process.env)) {
61902
62857
  process.env[m[1]] = m[2].replace(/^["']|["']$/g, "");
@@ -61942,7 +62897,7 @@ async function serveCommand() {
61942
62897
  staticDir: outDir,
61943
62898
  config,
61944
62899
  ...config?.rateLimit ? { rateLimit: config.rateLimit } : {},
61945
- ...typeof config?.compression === "boolean" ? { compression: config.compression } : {},
62900
+ ...config?.compression !== void 0 || config?.server?.compression !== void 0 ? { compression: config?.compression ?? config?.server?.compression } : {},
61946
62901
  ...pluginManager && pluginManager.size() > 0 ? { plugins: pluginManager } : {},
61947
62902
  ...config?.images ? { images: config.images } : {},
61948
62903
  ...config?.security ? { security: config.security } : {}
@@ -62221,7 +63176,7 @@ async function adapterCommand() {
62221
63176
  // tw/commands/test.ts
62222
63177
  import { join as join27 } from "node:path";
62223
63178
  import { createRequire as _cr } from "node:module";
62224
- import { existsSync as existsSync22, readdirSync as readdirSync9, statSync as statSync9 } from "node:fs";
63179
+ import { existsSync as existsSync22, readdirSync as readdirSync9, statSync as statSync9, readFileSync as readFileSync15 } from "node:fs";
62225
63180
  var _require = _cr(import.meta.url);
62226
63181
  function collectTests(dir, out) {
62227
63182
  if (!existsSync22(dir)) return;
@@ -62277,6 +63232,17 @@ async function testCommand() {
62277
63232
  }
62278
63233
  }
62279
63234
  console.log("\n tw test -- running " + files.length + " test file" + (files.length === 1 ? "" : "s") + "\n");
63235
+ for (const f of files) {
63236
+ try {
63237
+ const src = readFileSync15(f, "utf-8");
63238
+ const hasBunTest = src.includes("bun:test");
63239
+ const hasTestCall = /\b(test|it|describe)\s*\(/.test(src);
63240
+ if (!hasBunTest && !hasTestCall) {
63241
+ console.log(" \x1B[33m! " + f.replace(rootDir + "/", "") + ' defined no tests -- only bun:test format runs (import { test, expect } from "bun:test")\x1B[0m');
63242
+ }
63243
+ } catch {
63244
+ }
63245
+ }
62280
63246
  if (files.length === 0) {
62281
63247
  console.log(" No tests found. Create tests/your-name.test.ts:");
62282
63248
  console.log(`
@@ -62309,8 +63275,8 @@ async function testCommand() {
62309
63275
 
62310
63276
  // tw/commands/check.ts
62311
63277
  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";
63278
+ import { join as join28, dirname as dirname8 } from "node:path";
63279
+ import { existsSync as existsSync23, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync10 } from "node:fs";
62314
63280
  async function checkCommand() {
62315
63281
  const args = process.argv.slice(3);
62316
63282
  const rootDir = process.cwd();
@@ -62332,7 +63298,7 @@ async function checkCommand() {
62332
63298
  for (const filePath of twFiles) {
62333
63299
  const relativePath = filePath.replace(rootDir + "/", "");
62334
63300
  try {
62335
- const source = readFileSync15(filePath, "utf-8");
63301
+ const source = readFileSync16(filePath, "utf-8");
62336
63302
  const result = await compile2(source, {
62337
63303
  filePath,
62338
63304
  optimize: false,
@@ -62343,6 +63309,42 @@ async function checkCommand() {
62343
63309
  const errors = diagnostics.filter((d) => d.severity === "error");
62344
63310
  const warnings = diagnostics.filter((d) => d.severity === "warning");
62345
63311
  const info = diagnostics.filter((d) => d.severity === "info");
63312
+ const checkSource = readFileSync16(filePath, "utf-8");
63313
+ const usedTags = /* @__PURE__ */ new Set();
63314
+ const collectUsed = (n) => {
63315
+ if (!n) return;
63316
+ if (n.type === "Element" || n.type === "Component") {
63317
+ const t2 = n.tag ?? n.name;
63318
+ if (typeof t2 === "string" && t2[0] === t2[0].toUpperCase()) usedTags.add(t2);
63319
+ }
63320
+ for (const c of n.children ?? []) collectUsed(c);
63321
+ if (n.type === "If" || n.type === "For" || n.type === "While") for (const c of n.body ?? []) collectUsed(c);
63322
+ };
63323
+ if (result.ast) {
63324
+ for (const n of result.ast.body ?? []) collectUsed(n);
63325
+ }
63326
+ const SAFE_TAGS = /* @__PURE__ */ new Set(["Link", "Suspense", "RouterLink", "Image", "optImage"]);
63327
+ const importedNames = /* @__PURE__ */ new Set();
63328
+ for (const im of checkSource.matchAll(/import\s+([A-Za-z_]\w*)\s+from/g)) importedNames.add(im[1]);
63329
+ for (const im of checkSource.matchAll(/import\s*\{([^}]+)\}/g)) {
63330
+ for (const part of im[1].split(",")) {
63331
+ const nm = part.split(" as ").pop().trim();
63332
+ if (nm) importedNames.add(nm);
63333
+ }
63334
+ }
63335
+ const unknownComponents = [];
63336
+ for (const tag of usedTags) {
63337
+ if (SAFE_TAGS.has(tag) || importedNames.has(tag)) continue;
63338
+ if (findComponentUp(dirname8(filePath), tag)) continue;
63339
+ unknownComponents.push(tag);
63340
+ }
63341
+ if (unknownComponents.length > 0) {
63342
+ console.debug(` ? ${relativePath} -- ${unknownComponents.length} unknown component(s)`);
63343
+ for (const t2 of unknownComponents) {
63344
+ console.debug(` ? TW098 unknown component <${t2}> -- no import resolves it and no components/${t2}.tw exists`);
63345
+ }
63346
+ totalWarnings += unknownComponents.length;
63347
+ }
62346
63348
  totalErrors += errors.length;
62347
63349
  totalWarnings += warnings.length;
62348
63350
  totalInfo += info.length;
@@ -62365,7 +63367,7 @@ async function checkCommand() {
62365
63367
  console.debug(" Compiler not found, running basic checks...\n");
62366
63368
  for (const filePath of twFiles) {
62367
63369
  const relativePath = filePath.replace(rootDir + "/", "");
62368
- const source = readFileSync15(filePath, "utf-8");
63370
+ const source = readFileSync16(filePath, "utf-8");
62369
63371
  const maskedSource = maskSourceStringsAndComments(source);
62370
63372
  const openBraces = (maskedSource.match(/\{/g) || []).length;
62371
63373
  const closeBraces = (maskedSource.match(/\}/g) || []).length;
@@ -62379,13 +63381,41 @@ async function checkCommand() {
62379
63381
  }
62380
63382
  console.log(`
62381
63383
  Summary: ${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info`);
63384
+ const argv = process.argv.slice(2);
63385
+ let maxWarnings = null;
63386
+ if (argv.includes("--strict")) maxWarnings = 0;
63387
+ const mwIdx = argv.indexOf("--max-warnings");
63388
+ if (mwIdx !== -1 && argv[mwIdx + 1] !== void 0 && /^\d+$/.test(argv[mwIdx + 1])) {
63389
+ maxWarnings = Number(argv[mwIdx + 1]);
63390
+ } else {
63391
+ const eq = argv.find((a) => a.startsWith("--max-warnings="));
63392
+ if (eq && /^\d+$/.test(eq.split("=")[1])) maxWarnings = Number(eq.split("=")[1]);
63393
+ }
62382
63394
  if (totalErrors > 0) {
62383
63395
  console.log(" Status: FAILED\n");
62384
63396
  process.exit(1);
63397
+ } else if (maxWarnings !== null && totalWarnings > maxWarnings) {
63398
+ console.log(` Status: FAILED -- ${totalWarnings} warnings exceed --max-warnings=${maxWarnings}
63399
+ `);
63400
+ process.exit(1);
62385
63401
  } else {
62386
63402
  console.log(" Status: PASSED\n");
62387
63403
  }
62388
63404
  }
63405
+ function findComponentUp(dir, name) {
63406
+ let cur = dir;
63407
+ for (let i = 0; i < 12 && cur && cur !== "/"; i++) {
63408
+ try {
63409
+ const candidate = join28(cur, "components", name + ".tw");
63410
+ if (existsSync23(candidate)) return true;
63411
+ } catch {
63412
+ }
63413
+ const parent = dirname8(cur);
63414
+ if (parent === cur) break;
63415
+ cur = parent;
63416
+ }
63417
+ return false;
63418
+ }
62389
63419
  function collectFiles2(dir, ext, results) {
62390
63420
  const entries = readdirSync10(dir);
62391
63421
  for (const entry of entries) {
@@ -62402,10 +63432,10 @@ function collectFiles2(dir, ext, results) {
62402
63432
  // tw/commands/plugin.ts
62403
63433
  init_tw();
62404
63434
  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";
63435
+ import { existsSync as existsSync24, readFileSync as readFileSync17, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, readdirSync as readdirSync11 } from "node:fs";
62406
63436
  function readConfigPlugins(cfgPath) {
62407
63437
  if (!existsSync24(cfgPath)) return [];
62408
- const src = stripCommentsStringAware(readFileSync16(cfgPath, "utf-8"));
63438
+ const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
62409
63439
  const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
62410
63440
  if (!m) return [];
62411
63441
  const out = [];
@@ -62424,7 +63454,7 @@ function addConfigPlugin(cfgPath, name) {
62424
63454
  `);
62425
63455
  return true;
62426
63456
  }
62427
- const src = stripCommentsStringAware(readFileSync16(cfgPath, "utf-8"));
63457
+ const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
62428
63458
  if (new RegExp(`["'\`]${name}["'\`]`).test(src)) return false;
62429
63459
  const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
62430
63460
  if (m) {
@@ -62441,7 +63471,7 @@ function addConfigPlugin(cfgPath, name) {
62441
63471
  }
62442
63472
  function removeConfigPlugin(cfgPath, name) {
62443
63473
  if (!existsSync24(cfgPath)) return false;
62444
- const src = stripCommentsStringAware(readFileSync16(cfgPath, "utf-8"));
63474
+ const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
62445
63475
  const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
62446
63476
  if (!m) return false;
62447
63477
  const kept = m[1].split(",").map((e) => e.trim()).filter((e) => e && e !== `"${name}"` && e !== `'${name}'` && e !== "`" + name + "`");
@@ -62645,13 +63675,13 @@ async function lspCommand() {
62645
63675
  }
62646
63676
 
62647
63677
  // tw/help.ts
62648
- import { readFileSync as readFileSync17 } from "node:fs";
63678
+ import { readFileSync as readFileSync18 } from "node:fs";
62649
63679
  import { join as join31 } from "node:path";
62650
63680
  function cliVersion() {
62651
63681
  try {
62652
63682
  const bd = globalThis.__TW_BUNDLE_DIR;
62653
63683
  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";
63684
+ return JSON.parse(readFileSync18(pj, "utf-8")).version ?? "0.0.0";
62655
63685
  } catch {
62656
63686
  return "0.0.0";
62657
63687
  }
@@ -62724,13 +63754,13 @@ var helpText = `
62724
63754
 
62725
63755
  // tw/commands/index.ts
62726
63756
  await init_build();
62727
- import { readFileSync as readFileSync18 } from "node:fs";
63757
+ import { readFileSync as readFileSync19 } from "node:fs";
62728
63758
  import { join as join32 } from "node:path";
62729
63759
  var VERSION = (() => {
62730
63760
  try {
62731
63761
  const bd = globalThis.__TW_BUNDLE_DIR;
62732
63762
  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";
63763
+ return JSON.parse(readFileSync19(pj, "utf-8")).version ?? "0.0.0";
62734
63764
  } catch {
62735
63765
  return "0.0.0";
62736
63766
  }