tw-framework 1.0.6 → 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
@@ -3,7 +3,9 @@ globalThis.__TW_BUNDLE_DIR = new URL(".", import.meta.url).pathname;
3
3
  import { createRequire } from "node:module";
4
4
  const require = createRequire(import.meta.url);
5
5
  var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
7
  var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
9
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
10
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
11
  }) : x)(function(x) {
@@ -17,6 +19,15 @@ var __export = (target, all) => {
17
19
  for (var name in all)
18
20
  __defProp(target, name, { get: all[name], enumerable: true });
19
21
  };
22
+ var __copyProps = (to, from, except, desc) => {
23
+ if (from && typeof from === "object" || typeof from === "function") {
24
+ for (let key of __getOwnPropNames(from))
25
+ if (!__hasOwnProp.call(to, key) && key !== except)
26
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
27
+ }
28
+ return to;
29
+ };
30
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
31
 
21
32
  // ../../packages/shared/tw/hashing/md5.ts
22
33
  function bytesToWords(bytes) {
@@ -2555,6 +2566,97 @@ var init_config = __esm({
2555
2566
  }
2556
2567
  });
2557
2568
 
2569
+ // ../../packages/shared/tw/source-mask.ts
2570
+ function blank(s) {
2571
+ return s.replace(/[^\n]/g, " ");
2572
+ }
2573
+ function maskSourceStringsAndComments(src) {
2574
+ let out = "";
2575
+ let i = 0;
2576
+ const n = src.length;
2577
+ while (i < n) {
2578
+ const ch = src[i];
2579
+ const next = src[i + 1];
2580
+ if (ch === "/" && next === "/") {
2581
+ let j = i;
2582
+ while (j < n && src[j] !== "\n") j++;
2583
+ out += blank(src.slice(i, j));
2584
+ i = j;
2585
+ continue;
2586
+ }
2587
+ if (ch === "/" && next === "*") {
2588
+ let j = i + 2;
2589
+ while (j < n && !(src[j] === "*" && src[j + 1] === "/")) j++;
2590
+ j = Math.min(j + 2, n);
2591
+ out += blank(src.slice(i, j));
2592
+ i = j;
2593
+ continue;
2594
+ }
2595
+ if (ch === '"' || ch === "'" || ch === "`") {
2596
+ let j = i + 1;
2597
+ while (j < n) {
2598
+ const c = src[j];
2599
+ if (c === "\\") {
2600
+ j += 2;
2601
+ continue;
2602
+ }
2603
+ if (c === ch) {
2604
+ j++;
2605
+ break;
2606
+ }
2607
+ j++;
2608
+ }
2609
+ out += blank(src.slice(i, j));
2610
+ i = j;
2611
+ continue;
2612
+ }
2613
+ out += ch;
2614
+ i++;
2615
+ }
2616
+ return out;
2617
+ }
2618
+ function stripCommentsStringAware(src) {
2619
+ let out = "";
2620
+ let i = 0;
2621
+ const n = src.length;
2622
+ while (i < n) {
2623
+ const c = src[i];
2624
+ if (c === '"' || c === "'") {
2625
+ const q = c;
2626
+ out += c;
2627
+ i++;
2628
+ while (i < n && src[i] !== q) {
2629
+ if (src[i] === "\\") {
2630
+ out += src[i++];
2631
+ if (i < n) out += src[i++];
2632
+ continue;
2633
+ }
2634
+ out += src[i++];
2635
+ }
2636
+ out += src[i] ?? "";
2637
+ i++;
2638
+ continue;
2639
+ }
2640
+ if (c === "/" && src[i + 1] === "/") {
2641
+ while (i < n && src[i] !== "\n") i++;
2642
+ continue;
2643
+ }
2644
+ if (c === "/" && src[i + 1] === "*") {
2645
+ const end = src.indexOf("*/", i + 2);
2646
+ i = end === -1 ? n : end + 2;
2647
+ out += " ";
2648
+ continue;
2649
+ }
2650
+ out += c;
2651
+ i++;
2652
+ }
2653
+ return out;
2654
+ }
2655
+ var init_source_mask = __esm({
2656
+ "../../packages/shared/tw/source-mask.ts"() {
2657
+ }
2658
+ });
2659
+
2558
2660
  // ../../packages/shared/tw/cache.ts
2559
2661
  function parseCacheBody(body) {
2560
2662
  const meta = {};
@@ -2577,9 +2679,12 @@ function parseCacheBody(body) {
2577
2679
  return meta;
2578
2680
  }
2579
2681
  function extractCacheDirective(source) {
2580
- const m = /page\s*\{[^}]*?\bcache\s*\{([^}]*)\}/.exec(source);
2682
+ const masked = maskSourceStringsAndComments(source);
2683
+ const m = /page\s*\{[^}]*?\bcache\s*\{([^}]*)\}/.exec(masked);
2581
2684
  if (!m) return null;
2582
- return parseCacheBody(m[1]);
2685
+ const bodyStart = m.index + m[0].length - m[1].length - 1;
2686
+ const bodyEnd = bodyStart + m[1].length;
2687
+ return parseCacheBody(source.slice(bodyStart, bodyEnd));
2583
2688
  }
2584
2689
  function cacheMetaIsUsable(meta) {
2585
2690
  if (!meta) return false;
@@ -2711,6 +2816,7 @@ function parseCacheProfilesSource(src) {
2711
2816
  var BUILTIN_CACHE_PROFILES;
2712
2817
  var init_cache = __esm({
2713
2818
  "../../packages/shared/tw/cache.ts"() {
2819
+ init_source_mask();
2714
2820
  BUILTIN_CACHE_PROFILES = {
2715
2821
  seconds: { stale: 1, revalidate: 1, expire: 2 },
2716
2822
  minutes: { stale: 60, revalidate: 300, expire: 3600 },
@@ -6653,6 +6759,7 @@ __export(tw_exports, {
6653
6759
  maskPhone: () => maskPhone,
6654
6760
  maskSSN: () => maskSSN,
6655
6761
  maskSensitiveData: () => maskSensitiveData,
6762
+ maskSourceStringsAndComments: () => maskSourceStringsAndComments,
6656
6763
  md5: () => md5,
6657
6764
  md5Async: () => md52,
6658
6765
  mergeConfig: () => mergeConfig,
@@ -6992,6 +7099,7 @@ __export(tw_exports, {
6992
7099
  shortHash: () => shortHash,
6993
7100
  signData: () => signData,
6994
7101
  statSync: () => statSync,
7102
+ stripCommentsStringAware: () => stripCommentsStringAware,
6995
7103
  supportsColor: () => supportsColor,
6996
7104
  supportsFileSystem: () => supportsFileSystem,
6997
7105
  supportsWebSocket: () => supportsWebSocket,
@@ -7072,6 +7180,7 @@ var init_tw = __esm({
7072
7180
  init_utils();
7073
7181
  init_config();
7074
7182
  init_cache();
7183
+ init_source_mask();
7075
7184
  init_constants();
7076
7185
  init_crypto();
7077
7186
  init_env();
@@ -9518,6 +9627,9 @@ function readEscape(source, start) {
9518
9627
  if (LINE_TERMINATORS.has(ch)) {
9519
9628
  return { value: "", end: start + 2 };
9520
9629
  }
9630
+ if (ch === "{" || ch === "}") {
9631
+ return { value: "\\" + ch, end: start + 2 };
9632
+ }
9521
9633
  return { value: ch, end: start + 2 };
9522
9634
  }
9523
9635
  function validateString(str) {
@@ -12848,6 +12960,7 @@ function parsePageDirective(cursor, token) {
12848
12960
  if (valToken && (valToken.type === "STRING" || valToken.type === "NUMBER")) {
12849
12961
  cursor.advance();
12850
12962
  value = valToken.value;
12963
+ if (valToken.type === "STRING") value = String(value).replace(/\\([{}"])/g, "$1");
12851
12964
  if (valToken.type === "NUMBER") value = parseFloat(valToken.value);
12852
12965
  } else if (cursor.match("EQUALS") || cursor.match("COLON")) {
12853
12966
  cursor.skipWhitespace();
@@ -12855,6 +12968,7 @@ function parsePageDirective(cursor, token) {
12855
12968
  if (vToken) {
12856
12969
  cursor.advance();
12857
12970
  value = vToken.value;
12971
+ if (vToken.type === "STRING") value = String(value).replace(/\\([{}"])/g, "$1");
12858
12972
  if (vToken.type === "NUMBER") value = parseFloat(vToken.value);
12859
12973
  if (vToken.type === "BOOLEAN") value = vToken.value === "true";
12860
12974
  }
@@ -12878,7 +12992,7 @@ function parsePageDirective(cursor, token) {
12878
12992
  const nextToken = cursor.peek();
12879
12993
  if (nextToken && (nextToken.type === "STRING" || nextToken.type === "NUMBER" || nextToken.type === "IDENT" || nextToken.type === "KEYWORD")) {
12880
12994
  cursor.advance();
12881
- options[optName] = nextToken.value;
12995
+ options[optName] = nextToken.type === "STRING" ? String(nextToken.value).replace(/\\([{}"])/g, "$1") : nextToken.value;
12882
12996
  } else if (cursor.match("EQUALS")) {
12883
12997
  cursor.skipWhitespace();
12884
12998
  const optVal = cursor.consumeValue();
@@ -13258,8 +13372,16 @@ function consumeSignalDeclaration(cursor) {
13258
13372
  function consumeStateValue(cursor) {
13259
13373
  const t2 = cursor.peek();
13260
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
+ }
13261
13382
  if (t2.type === "STRING" || t2.type === "NUMBER" || t2.type === "IDENT" || t2.type === "KEYWORD") {
13262
13383
  cursor.advance();
13384
+ if (t2.type === "STRING") return String(t2.value).replace(/\\([{}"])/g, "$1");
13263
13385
  return t2.value;
13264
13386
  }
13265
13387
  if (t2.type === "LBRACKET" || t2.type === "LBRACE" || t2.type === "LBRACK") {
@@ -13272,7 +13394,7 @@ function consumeStateValue(cursor) {
13272
13394
  else if (tk.type === "RBRACKET" || tk.type === "RBRACE" || tk.type === "RBRACK") depth--;
13273
13395
  const after = cursor.peek(1);
13274
13396
  if (tk.type === "STRING") {
13275
- parts.push(`"${tk.value}"`);
13397
+ parts.push(`"${String(tk.value).replace(/\\([{}"])/g, "$1")}"`);
13276
13398
  } else if (tk.type === "IDENT" && after && after.type === "COLON" && depth > 0) {
13277
13399
  parts.push(`"${tk.value}"`);
13278
13400
  } else {
@@ -14274,6 +14396,20 @@ function parseTWSelector(cursor, errors, state, parseBody) {
14274
14396
  }
14275
14397
  cursor.consumeIf("RBRACE");
14276
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
+ }
14277
14413
  el.children = parseBody(["RBRACE"]);
14278
14414
  cursor.consumeIf("RBRACE");
14279
14415
  }
@@ -14892,10 +15028,13 @@ function parseWhileStatement(cursor, errors, parseBody) {
14892
15028
  const whileToken = cursor.advance();
14893
15029
  if (!whileToken) return null;
14894
15030
  cursor.skipWhitespace();
14895
- const condToken = cursor.peek();
14896
15031
  let condition = "";
14897
- if (condToken) {
14898
- 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;
14899
15038
  cursor.advance();
14900
15039
  }
14901
15040
  const body = parseBlockBody2(cursor, parseBody);
@@ -15016,13 +15155,133 @@ function parseBlockBody2(cursor, parseBody) {
15016
15155
  }
15017
15156
  return parseBody(["KEYWORD", "RBRACE"]);
15018
15157
  }
15019
- var CLASS_BOUNDARY_BOOLEAN_ATTRS;
15158
+ var ATTR_PAIR_NAMES, CLASS_BOUNDARY_BOOLEAN_ATTRS;
15020
15159
  var init_statements = __esm({
15021
15160
  "../../packages/compiler/tw/parser/statements/index.ts"() {
15022
15161
  init_nodes();
15023
15162
  init_recovery();
15024
15163
  init_directives();
15025
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
+ ]);
15026
15285
  CLASS_BOUNDARY_BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
15027
15286
  "disabled",
15028
15287
  "required",
@@ -21274,6 +21533,15 @@ function splitArgs2(str) {
21274
21533
  function findMatchingBrace3(str, start) {
21275
21534
  let depth = 1;
21276
21535
  for (let i = start + 1; i < str.length; i++) {
21536
+ if (str[i] === '"' || str[i] === "'") {
21537
+ const q = str[i];
21538
+ i++;
21539
+ while (i < str.length && str[i] !== q) {
21540
+ if (str[i] === "\\") i++;
21541
+ i++;
21542
+ }
21543
+ continue;
21544
+ }
21277
21545
  if (str[i] === "{") depth++;
21278
21546
  else if (str[i] === "}") {
21279
21547
  depth--;
@@ -22826,7 +23094,7 @@ var init_types4 = __esm({
22826
23094
  var TW_VERSION, TW_GENERATOR_META;
22827
23095
  var init_version = __esm({
22828
23096
  "../../packages/compiler/tw/codegen/version.ts"() {
22829
- TW_VERSION = "1.0.6";
23097
+ TW_VERSION = "1.0.8";
22830
23098
  TW_GENERATOR_META = `<meta name="generator" content="TW Framework ${TW_VERSION}">`;
22831
23099
  }
22832
23100
  });
@@ -23213,6 +23481,16 @@ function collectBuiltinImports(program) {
23213
23481
  for (const dir of program.directives ?? []) {
23214
23482
  if (dir.type !== "ImportDirective") continue;
23215
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
+ }
23216
23494
  if (!BUILTIN_COMPONENT_SPECIFIERS.includes(imp.source)) continue;
23217
23495
  if (imp.defaultImport) out.set(imp.defaultImport, imp.source);
23218
23496
  for (const item of imp.items ?? []) {
@@ -23282,7 +23560,7 @@ function normalizePrefetchProp(props) {
23282
23560
  return p;
23283
23561
  }
23284
23562
  function generateRouterLinkTag(props, textContent) {
23285
- const href = (props.href ?? "").trim();
23563
+ const href = (props.href ?? props.to ?? "").trim();
23286
23564
  if (!href) return `<!-- @tw/RouterLink: missing href -->`;
23287
23565
  const opts = {
23288
23566
  href,
@@ -23309,13 +23587,18 @@ function generateBuiltinTag(specifier, props, textContent) {
23309
23587
  if (specifier === "@tw/RouterLink") return generateRouterLinkTag(props, textContent);
23310
23588
  return null;
23311
23589
  }
23312
- var BUILTIN_COMPONENT_SPECIFIERS, imageConfig;
23590
+ var BUILTIN_COMPONENT_SPECIFIERS, imageConfig, TW_NAMED_BUILTINS;
23313
23591
  var init_builtin_components = __esm({
23314
23592
  "../../packages/compiler/tw/codegen/builtin-components.ts"() {
23315
23593
  init_tw2();
23316
23594
  init_tw3();
23317
23595
  BUILTIN_COMPONENT_SPECIFIERS = ["@tw/optImage", "@tw/RouterLink"];
23318
23596
  imageConfig = DEFAULT_IMAGE_CONFIG;
23597
+ TW_NAMED_BUILTINS = {
23598
+ RouterLink: "@tw/RouterLink",
23599
+ optImage: "@tw/optImage",
23600
+ Image: "@tw/optImage"
23601
+ };
23319
23602
  }
23320
23603
  });
23321
23604
 
@@ -23386,6 +23669,14 @@ function interpolate(template, vars) {
23386
23669
  let i = 0;
23387
23670
  while (i < template.length) {
23388
23671
  const ch = template[i];
23672
+ if (ch === "\\") {
23673
+ const nxt = template[i + 1];
23674
+ if (nxt === "{" || nxt === "}") {
23675
+ result += nxt;
23676
+ i += 2;
23677
+ continue;
23678
+ }
23679
+ }
23389
23680
  if (ch === "{" && template[i + 1] !== "{") {
23390
23681
  let depth = 1;
23391
23682
  let j = i + 1;
@@ -23874,7 +24165,8 @@ function validatePlainCss(src, filePath) {
23874
24165
  function compileDecl(decl) {
23875
24166
  const trimmed = decl.trim();
23876
24167
  if (!trimmed) return "";
23877
- if (trimmed.includes(":")) return trimmed.replace(/\s*:\s*/, ": ");
24168
+ const cssForm = /^([-\w]+)\s*:\s*/.exec(trimmed);
24169
+ if (cssForm) return trimmed.replace(/^([-\w]+)\s*:\s*/, "$1: ");
23878
24170
  const spaceIdx = trimmed.search(/[\s]/);
23879
24171
  if (spaceIdx === -1) {
23880
24172
  return TSS_SHORTHANDS2[trimmed] ?? trimmed;
@@ -23888,9 +24180,124 @@ function combineSelector(parent, child) {
23888
24180
  if (child.includes("&")) return child.replace(/&/g, parent);
23889
24181
  return `${parent} ${child}`;
23890
24182
  }
24183
+ function stripTssComments(src) {
24184
+ let out = "";
24185
+ let i = 0;
24186
+ const n = src.length;
24187
+ while (i < n) {
24188
+ const c = src[i];
24189
+ if (c === '"' || c === "'") {
24190
+ const q = c;
24191
+ out += c;
24192
+ i++;
24193
+ while (i < n && src[i] !== q) {
24194
+ if (src[i] === "\\") {
24195
+ out += src[i++];
24196
+ if (i < n) out += src[i++];
24197
+ continue;
24198
+ }
24199
+ out += src[i++];
24200
+ }
24201
+ out += src[i] ?? "";
24202
+ i++;
24203
+ continue;
24204
+ }
24205
+ if (c === "u" && /^url\s*\(/i.test(src.slice(i, i + 6))) {
24206
+ let depth = 0;
24207
+ let j = i;
24208
+ for (; j < n; j++) {
24209
+ if (src[j] === "(") depth++;
24210
+ else if (src[j] === ")") {
24211
+ depth--;
24212
+ if (depth === 0) {
24213
+ j++;
24214
+ break;
24215
+ }
24216
+ }
24217
+ }
24218
+ out += src.slice(i, j);
24219
+ i = j;
24220
+ continue;
24221
+ }
24222
+ if (c === "/" && src[i + 1] === "/") {
24223
+ while (i < n && src[i] !== "\n") i++;
24224
+ continue;
24225
+ }
24226
+ if (c === "/" && src[i + 1] === "*") {
24227
+ const end = src.indexOf("*/", i + 2);
24228
+ i = end === -1 ? n : end + 2;
24229
+ out += " ";
24230
+ continue;
24231
+ }
24232
+ out += c;
24233
+ i++;
24234
+ }
24235
+ return out;
24236
+ }
23891
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
+ }
24290
+ const __twOut = __twCompileTSSInner(source);
24291
+ const leftover = __twOut.match(/\$[A-Za-z_][\w-]*/g);
24292
+ if (leftover && !/\$\{/.test(__twOut)) {
24293
+ const seen = Array.from(new Set(leftover));
24294
+ console.warn("[tss] unsubstituted $var(s) in output: " + seen.join(", ") + " -- define them as state vars or fix the spelling");
24295
+ }
24296
+ return __twOut;
24297
+ }
24298
+ function __twCompileTSSInner(source) {
23892
24299
  if (!source || !source.trim()) return "";
23893
- let src = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
24300
+ let src = stripTssComments(source);
23894
24301
  const vars = {};
23895
24302
  src = src.replace(/\$([A-Za-z_][\w-]*)\s*:\s*([^;{}\n]+);?/g, (_m, name, val) => {
23896
24303
  vars[name] = String(val).trim();
@@ -24100,6 +24507,11 @@ function stripComments(src) {
24100
24507
  out += c;
24101
24508
  i++;
24102
24509
  while (i < n && src[i] !== q) {
24510
+ if (src[i] === "\\") {
24511
+ out += src[i++];
24512
+ if (i < n) out += src[i++];
24513
+ continue;
24514
+ }
24103
24515
  out += src[i++];
24104
24516
  }
24105
24517
  out += src[i] ?? "";
@@ -24418,9 +24830,19 @@ function emit(items, parent, env, mixins, out) {
24418
24830
  out.push(...childCss);
24419
24831
  }
24420
24832
  function compileSCSS(source) {
24833
+ const cleaned0 = stripComments(source);
24834
+ const UNSUPPORTED = /@(import|extend|use|forward|function|each|if|else|for|while)\b/g;
24835
+ const m = cleaned0.match(UNSUPPORTED);
24836
+ if (m) {
24837
+ const seen = Array.from(new Set(m.map((x) => x.trim())));
24838
+ console.warn("[scss] unsupported at-rule(s) ignored: " + seen.join(", ") + " -- TW's SCSS subset supports nesting, $vars, & references and @mixin/@include");
24839
+ }
24421
24840
  const items = parseItems(stripComments(source));
24422
24841
  const out = [];
24423
24842
  emit(items, "", [/* @__PURE__ */ new Map()], /* @__PURE__ */ new Map(), out);
24843
+ if (out.length === 0 && cleaned0.replace(/[\s;]/g, "").length > 0) {
24844
+ console.warn("[scss] compileSCSS produced no output for a non-empty source -- check for unsupported syntax (TW uses `color: #fff` declarations, not `color #fff`)");
24845
+ }
24424
24846
  return out.join("\n");
24425
24847
  }
24426
24848
  var init_scss = __esm({
@@ -24460,7 +24882,8 @@ function addScope(css, scopeId) {
24460
24882
  return css.replace(/([.#]?[\w-]+)\s*\{/g, `$1[data-tw-scope="${scopeId}"] {`);
24461
24883
  }
24462
24884
  function interpolate2(expr, vars) {
24463
- return expr.replace(/\{([^}]+)\}/g, (_, name) => {
24885
+ const masked = expr.replace(/\\\{/g, "O").replace(/\\\}/g, "C");
24886
+ const out = masked.replace(/\{([^}]+)\}/g, (_, name) => {
24464
24887
  const key = name.trim();
24465
24888
  if (/^[a-zA-Z_$][\w$]*$/.test(key)) {
24466
24889
  return vars[key] ?? "";
@@ -24468,6 +24891,7 @@ function interpolate2(expr, vars) {
24468
24891
  const val = evaluate(key, vars);
24469
24892
  return val === null ? "" : val;
24470
24893
  });
24894
+ return out.replace(/\u0001O/g, "{").replace(/\u0001C/g, "}");
24471
24895
  }
24472
24896
  function isTruthy2(val) {
24473
24897
  return isTruthy(val);
@@ -24553,13 +24977,13 @@ function collectTssImports(programs, ctx) {
24553
24977
  else ctx.inlineStyles.push(css);
24554
24978
  }
24555
24979
  } catch (e) {
24556
- 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;
24557
24981
  }
24558
24982
  }
24559
24983
  }
24560
24984
  }
24561
24985
  } catch (e) {
24562
- 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;
24563
24987
  }
24564
24988
  }
24565
24989
  function resolveTssPath(fromDir, rel) {
@@ -24779,6 +25203,10 @@ function generateSuspense(el, ctx) {
24779
25203
  function generateElement(el, ctx) {
24780
25204
  const tag = el.tag;
24781
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
+ }
24782
25210
  {
24783
25211
  const builtinSpec = resolveBuiltin(tag, activeBuiltinImports);
24784
25212
  if (builtinSpec) {
@@ -24869,6 +25297,8 @@ function renderAttrs(el, ctx) {
24869
25297
  } else if (typeof attr.value === "string" && !attr.name.startsWith("data-tw-event") && /\{[^}]+\}/.test(attr.value) && Object.keys(ctx.stateVars).length > 0) {
24870
25298
  const val = interpolate2(attr.value, ctx.stateVars);
24871
25299
  parts.push(`${attr.name}="${escapeAttr2(sanitizeUrlAttr(attr.name, val))}"`);
25300
+ } else if (typeof attr.value === "string" && /\\[{}]/.test(attr.value)) {
25301
+ parts.push(`${attr.name}="${escapeAttr2(sanitizeUrlAttr(attr.name, String(attr.value).replace(/\\([{}])/g, "$1")))}"`);
24872
25302
  } else {
24873
25303
  parts.push(`${attr.name}="${escapeAttr2(sanitizeUrlAttr(attr.name, attr.value))}"`);
24874
25304
  }
@@ -24975,6 +25405,9 @@ function generateText(text, ctx) {
24975
25405
  }
24976
25406
  return escapeHTML(interpolate2(text.value, ctx.stateVars));
24977
25407
  }
25408
+ if (text.value && /\\[{}]/.test(text.value)) {
25409
+ return escapeHTML(text.value.replace(/\\([{}])/g, "$1"));
25410
+ }
24978
25411
  return escapeHTML(text.value);
24979
25412
  }
24980
25413
  function registerComponentTemplate(name, program) {
@@ -25033,7 +25466,13 @@ function generateComponent(comp, ctx) {
25033
25466
  ctx.slotContent = savedSlot;
25034
25467
  ctx.inHead = compSavedInHead;
25035
25468
  } else {
25036
- 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
+ }
25037
25476
  }
25038
25477
  ctx.stateVars = savedStateVars;
25039
25478
  ctx.componentStack.pop();
@@ -26458,7 +26897,7 @@ function emitElement(gen, el, ctx) {
26458
26897
  attrStr = el.attrs.map((a) => {
26459
26898
  if (a.value === true) return a.name;
26460
26899
  const val = a.isInterpolated ? interpolate3(a.value, ctx.stateVars) : a.value;
26461
- return `${a.name}="${escapeAttr3(val)}"`;
26900
+ return `${a.name}="${escapeAttr3(sanitizeUrlAttr2(a.name, val))}"`;
26462
26901
  }).join(" ");
26463
26902
  }
26464
26903
  if (el.styles && el.styles.length > 0) {
@@ -26574,14 +27013,25 @@ function getDirectiveValue2(program, key) {
26574
27013
  function escapeHTML2(str) {
26575
27014
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
26576
27015
  }
27016
+ function sanitizeUrlAttr2(name, value) {
27017
+ const URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "poster", "background"]);
27018
+ if (!URL_ATTRS.has(name.toLowerCase())) return value;
27019
+ const trimmed = String(value).trim().toLowerCase().replace(/[\s\0]/g, "");
27020
+ if (trimmed.startsWith("javascript:") || trimmed.startsWith("data:text/html") || trimmed.startsWith("vbscript:")) {
27021
+ return "#";
27022
+ }
27023
+ return value;
27024
+ }
26577
27025
  function escapeAttr3(str) {
26578
27026
  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
26579
27027
  }
26580
27028
  function interpolate3(expr, vars) {
26581
- return expr.replace(/\{([^}]+)\}/g, (_, name) => {
27029
+ const masked = expr.replace(/\\\{/g, "O").replace(/\\\}/g, "C");
27030
+ const out = masked.replace(/\{([^}]+)\}/g, (_, name) => {
26582
27031
  const key = name.trim();
26583
27032
  return vars[key] ?? "";
26584
27033
  });
27034
+ return out.replace(/\u0001O/g, "{").replace(/\u0001C/g, "}");
26585
27035
  }
26586
27036
  function isTruthy3(val) {
26587
27037
  if (!val) return false;
@@ -26785,6 +27235,15 @@ var init_streaming = __esm({
26785
27235
  });
26786
27236
 
26787
27237
  // ../../packages/compiler/tw/codegen/vdom-gen.ts
27238
+ function sanitizeUrlAttr3(name, value) {
27239
+ const URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "poster", "background"]);
27240
+ if (!URL_ATTRS.has(name.toLowerCase())) return value;
27241
+ const trimmed = String(value).trim().toLowerCase().replace(/[\s\0]/g, "");
27242
+ if (trimmed.startsWith("javascript:") || trimmed.startsWith("data:text/html") || trimmed.startsWith("vbscript:")) {
27243
+ return "#";
27244
+ }
27245
+ return value;
27246
+ }
26788
27247
  function generateVDOMCode(program, ctx) {
26789
27248
  const builder2 = new VDOMCodeBuilder(ctx);
26790
27249
  return builder2.generateModule(program);
@@ -26798,10 +27257,12 @@ function generateNodeVDOM(node, ctx) {
26798
27257
  return builder2.generateFromAST(node);
26799
27258
  }
26800
27259
  function interpolate4(expr, vars) {
26801
- return expr.replace(/\{([^}]+)\}/g, (_, name) => {
27260
+ const masked = expr.replace(/\\\{/g, "O").replace(/\\\}/g, "C");
27261
+ const out = masked.replace(/\{([^}]+)\}/g, (_, name) => {
26802
27262
  const key = name.trim();
26803
27263
  return vars[key] ?? "";
26804
27264
  });
27265
+ return out.replace(/\u0001O/g, "{").replace(/\u0001C/g, "}");
26805
27266
  }
26806
27267
  function isTruthy4(val) {
26807
27268
  if (!val) return false;
@@ -26935,7 +27396,7 @@ var init_vdom_gen = __esm({
26935
27396
  const expr = this.interpolateToExpr(attr.value);
26936
27397
  attrs.push(`${JSON.stringify(attr.name)}: ${expr}`);
26937
27398
  } else {
26938
- attrs.push(`${JSON.stringify(attr.name)}: ${JSON.stringify(attr.value)}`);
27399
+ attrs.push(`${JSON.stringify(attr.name)}: ${JSON.stringify(sanitizeUrlAttr3(attr.name, attr.value))}`);
26939
27400
  }
26940
27401
  }
26941
27402
  }
@@ -28806,13 +29267,27 @@ ${indentation})` : type;
28806
29267
  });
28807
29268
 
28808
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
+ }
28809
29284
  function generateWithLayout(layoutProgram, pageProgram, stateVars) {
28810
29285
  const ctx = createContext("ssr");
28811
29286
  if (stateVars) ctx.stateVars = { ...stateVars };
28812
29287
  for (const dir of pageProgram.directives || []) {
28813
29288
  if (dir.type === "StateDirective") {
28814
29289
  for (const decl of dir.declarations || []) {
28815
- ctx.stateVars[decl.name] = decl.value;
29290
+ ctx.stateVars[decl.name] = stateLiteralForRender(decl.value);
28816
29291
  }
28817
29292
  }
28818
29293
  }
@@ -28844,7 +29319,7 @@ function generateWithLayoutChain(layoutPrograms, pageProgram, stateVars) {
28844
29319
  for (const dir of pageProgram.directives || []) {
28845
29320
  if (dir.type === "StateDirective") {
28846
29321
  for (const decl of dir.declarations || []) {
28847
- ctx2.stateVars[decl.name] = decl.value;
29322
+ ctx2.stateVars[decl.name] = stateLiteralForRender(decl.value);
28848
29323
  }
28849
29324
  }
28850
29325
  }
@@ -28952,7 +29427,7 @@ function generateVDOM(program, ctx) {
28952
29427
  return {
28953
29428
  t: el.tag,
28954
29429
  a: el.attrs.reduce((acc, a) => {
28955
- acc[a.name] = a.value;
29430
+ acc[a.name] = sanitizeUrlAttr4(a.name, String(a.value));
28956
29431
  return acc;
28957
29432
  }, {}),
28958
29433
  c: el.children.map(toVNode)
@@ -28990,16 +29465,27 @@ function processDirectives(program, ctx) {
28990
29465
  }
28991
29466
  }
28992
29467
  function interpolate5(expr, vars) {
28993
- return expr.replace(/\{([^}]+)\}/g, (_, name) => {
29468
+ const masked = expr.replace(/\\\{/g, "O").replace(/\\\}/g, "C");
29469
+ const out = masked.replace(/\{([^}]+)\}/g, (_, name) => {
28994
29470
  const key = name.trim();
28995
29471
  return vars[key] ?? "";
28996
29472
  });
29473
+ return out.replace(/\u0001O/g, "{").replace(/\u0001C/g, "}");
28997
29474
  }
28998
29475
  function isTruthy5(val) {
28999
29476
  if (!val) return false;
29000
29477
  const lower = val.toLowerCase().trim();
29001
29478
  return lower !== "false" && lower !== "0" && lower !== "" && lower !== "null" && lower !== "undefined";
29002
29479
  }
29480
+ function sanitizeUrlAttr4(name, value) {
29481
+ const URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "poster", "background"]);
29482
+ if (!URL_ATTRS.has(name.toLowerCase())) return value;
29483
+ const trimmed = String(value).trim().toLowerCase().replace(/[\s\0]/g, "");
29484
+ if (trimmed.startsWith("javascript:") || trimmed.startsWith("data:text/html") || trimmed.startsWith("vbscript:")) {
29485
+ return "#";
29486
+ }
29487
+ return value;
29488
+ }
29003
29489
  function generate(program, stateVars) {
29004
29490
  const startTime = performance.now();
29005
29491
  const ctx = createContext("ssr");
@@ -29024,7 +29510,9 @@ function generate(program, stateVars) {
29024
29510
  ctx.hasInteractivity = true;
29025
29511
  continue;
29026
29512
  }
29027
- ctx.stateVars[decl.name] = decl.value;
29513
+ if (!(decl.name in ctx.stateVars)) {
29514
+ ctx.stateVars[decl.name] = stateLiteralForRender(decl.value);
29515
+ }
29028
29516
  let v = decl.value;
29029
29517
  try {
29030
29518
  v = JSON.parse(decl.value);
@@ -29357,7 +29845,11 @@ var init_codes = __esm({
29357
29845
  TW090: { message: "cache { } requires a revalidate window or a life profile", severity: "error", category: "syntax" },
29358
29846
  TW091: { message: "fn cached handler is impure (request.cookies/headers/body or setSignal)", severity: "error", category: "semantic" },
29359
29847
  TW092: { message: "Unknown cache profile", severity: "error", category: "semantic" },
29360
- 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" }
29361
29853
  };
29362
29854
  }
29363
29855
  });
@@ -29678,6 +30170,124 @@ function checkUnusedDeclarations(program, filePath, diags) {
29678
30170
  }
29679
30171
  }
29680
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
+ }
29681
30291
  var HTML_ELEMENTS2, HTML_ATTRIBUTES2, GLOBAL_ATTRIBUTES2, CSS_PROPERTIES2, CSS_ALIASES2, EVENT_TYPES2, ARIA_ROLES2, ARIA_ATTRIBUTES2;
29682
30292
  var init_rules = __esm({
29683
30293
  "../../packages/compiler/tw/diagnostics/rules.ts"() {
@@ -30568,6 +31178,10 @@ function diagnose(program, filePath) {
30568
31178
  checkSemanticErrors(program, filePath, diagnostics);
30569
31179
  checkDirectives(program, filePath, diagnostics);
30570
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);
30571
31185
  return diagnostics;
30572
31186
  }
30573
31187
  var RuleEngine;
@@ -33080,7 +33694,12 @@ async function renderIslandContent(component, props, ctx) {
33080
33694
  return { html: result.html, css: result.css, js: result.js };
33081
33695
  }
33082
33696
  function countServerComponents(source) {
33083
- const matches = source.match(/@render\s+mode:\s*['"]server['"]/g);
33697
+ let cleaned = source;
33698
+ try {
33699
+ cleaned = stripCommentsStringAware(source);
33700
+ } catch {
33701
+ }
33702
+ const matches = cleaned.match(/@render\s+mode:\s*['"]server['"]/g);
33084
33703
  return matches ? matches.length : 0;
33085
33704
  }
33086
33705
  function determineHydrationStrategy(component, position) {
@@ -33118,6 +33737,7 @@ function parseClientDirective(source) {
33118
33737
  var componentRegistry2;
33119
33738
  var init_server_components = __esm({
33120
33739
  "../../packages/compiler/tw/server-components.ts"() {
33740
+ init_tw();
33121
33741
  init_tw4();
33122
33742
  componentRegistry2 = /* @__PURE__ */ new Map();
33123
33743
  }
@@ -40041,385 +40661,6 @@ var init_tw4 = __esm({
40041
40661
  }
40042
40662
  });
40043
40663
 
40044
- // ../../packages/shared/tw/node-import.ts
40045
- var node_import_exports = {};
40046
- __export(node_import_exports, {
40047
- twImportTs: () => twImportTs
40048
- });
40049
- import { join as join10 } from "node:path";
40050
- import { tmpdir } from "node:os";
40051
- import { pathToFileURL as pathToFileURL2 } from "node:url";
40052
- async function twImportTs(tsPath) {
40053
- if (!globalThis.__TW_NODE) {
40054
- return import(tsPath);
40055
- }
40056
- const esbuildMain = globalThis.__TW_ESBUILD_MODULE;
40057
- const esbuild = await (esbuildMain ? import(esbuildMain) : import("esbuild"));
40058
- const safe = tsPath.replace(/[^a-zA-Z0-9]/g, "_").slice(-120);
40059
- const cachePath = join10(tmpdir(), "tw-" + safe + ".mjs");
40060
- await esbuild.build({
40061
- entryPoints: [tsPath],
40062
- bundle: true,
40063
- format: "esm",
40064
- platform: "node",
40065
- target: "node18",
40066
- outfile: cachePath,
40067
- logLevel: "silent"
40068
- });
40069
- return import(pathToFileURL2(cachePath).href);
40070
- }
40071
- var init_node_import = __esm({
40072
- "../../packages/shared/tw/node-import.ts"() {
40073
- }
40074
- });
40075
-
40076
- // tw/commands/client-bundle.ts
40077
- var client_bundle_exports = {};
40078
- __export(client_bundle_exports, {
40079
- BUILTIN_CLIENT_OK_SPECIFIERS: () => BUILTIN_CLIENT_OK_SPECIFIERS,
40080
- RUNTIME_CLIENT_OK_SPECIFIERS: () => RUNTIME_CLIENT_OK_SPECIFIERS,
40081
- buildClientChunk: () => buildClientChunk,
40082
- buildPageScope: () => buildPageScope,
40083
- collectClientImportsFromSource: () => collectClientImportsFromSource,
40084
- findServerOnlyViolations: () => findServerOnlyViolations,
40085
- importNames: () => importNames,
40086
- isClientImportSpecifier: () => isClientImportSpecifier
40087
- });
40088
- import { join as join11, resolve as resolve3 } from "node:path";
40089
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
40090
- import { spawnSync as spawnSync2 } from "node:child_process";
40091
- function isClientImportSpecifier(spec) {
40092
- if (spec.startsWith("@./") || spec.startsWith("./") || spec.startsWith("../")) return false;
40093
- if (/\.(tw|tss|twm|ts|js)$/.test(spec)) return false;
40094
- if (spec.startsWith("@tw/")) {
40095
- return RUNTIME_CLIENT_OK_SPECIFIERS.includes(spec);
40096
- }
40097
- return true;
40098
- }
40099
- function collectClientImportsFromSource(src) {
40100
- const lines = [];
40101
- let m;
40102
- IMPORT_RE.lastIndex = 0;
40103
- while (m = IMPORT_RE.exec(src)) {
40104
- if (isClientImportSpecifier(m[2])) lines.push(m[0].trim().replace(/;$/, ""));
40105
- }
40106
- return lines;
40107
- }
40108
- function importNames(line) {
40109
- const clause = line.match(/^import\s+([\s\S]+?)\s+from/);
40110
- if (!clause) return [];
40111
- const c = clause[1].trim();
40112
- const names = [];
40113
- if (c.startsWith("*")) {
40114
- const as = c.split(/\s+as\s+/);
40115
- if (as[1]) names.push(as[1].trim());
40116
- } else if (c.startsWith("{")) {
40117
- const braces = c.match(/\{([\s\S]*)\}/);
40118
- if (braces) {
40119
- for (const part of braces[1].split(",")) {
40120
- const p = part.trim();
40121
- if (!p) continue;
40122
- const seg = p.split(/\s+as\s+/);
40123
- names.push(seg[seg.length - 1].trim());
40124
- }
40125
- }
40126
- } else {
40127
- const def = c.match(/^[A-Za-z_$][\w$]*/);
40128
- if (def) names.push(def[0]);
40129
- }
40130
- return names;
40131
- }
40132
- function findEsbuildBin(rootDir) {
40133
- const candidates = [
40134
- join11(rootDir, "node_modules", ".bin", "esbuild"),
40135
- resolve3(rootDir, "..", "node_modules", ".bin", "esbuild"),
40136
- // CLI repo layout (dev / source runs)
40137
- resolve3(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "node_modules", ".bin", "esbuild")
40138
- ];
40139
- for (const c of candidates) {
40140
- try {
40141
- if (existsSync7(c)) return c;
40142
- } catch {
40143
- }
40144
- }
40145
- const bd = globalThis.__TW_BUNDLE_DIR;
40146
- if (bd) {
40147
- const b = resolve3(bd, "..", "..", "..", "node_modules", ".bin", "esbuild");
40148
- try {
40149
- if (existsSync7(b)) return b;
40150
- } catch {
40151
- }
40152
- }
40153
- return "esbuild";
40154
- }
40155
- function findServerOnlyViolations(src) {
40156
- const out = [];
40157
- let m;
40158
- IMPORT_RE.lastIndex = 0;
40159
- while (m = IMPORT_RE.exec(src)) {
40160
- const spec = m[2];
40161
- if (spec === "lib" || spec.startsWith("lib/")) {
40162
- out.push(spec);
40163
- continue;
40164
- }
40165
- if (isClientImportSpecifier(spec)) continue;
40166
- if (spec.startsWith("@tw/")) {
40167
- if (BUILTIN_CLIENT_OK_SPECIFIERS.includes(spec)) continue;
40168
- if (RUNTIME_CLIENT_OK_SPECIFIERS.includes(spec)) continue;
40169
- out.push(spec);
40170
- continue;
40171
- }
40172
- if (spec.startsWith("@./") || spec.startsWith("./") || spec.startsWith("../")) {
40173
- if (/\.(tw|tss)$/.test(spec)) continue;
40174
- out.push(spec);
40175
- continue;
40176
- }
40177
- if (spec.startsWith("lib/")) out.push(spec);
40178
- }
40179
- return out;
40180
- }
40181
- function contentHash2(s) {
40182
- let h = 5381;
40183
- for (let i = 0; i < s.length; i++) h = (h * 33 ^ s.charCodeAt(i)) >>> 0;
40184
- return h.toString(36);
40185
- }
40186
- function vendoredRuntimeClient() {
40187
- const fromSrc = new URL("../../dist/tw-runtime-client.mjs", import.meta.url).pathname;
40188
- if (existsSync7(fromSrc)) return fromSrc;
40189
- const fromBundle = new URL("./tw-runtime-client.mjs", import.meta.url).pathname;
40190
- if (existsSync7(fromBundle)) return fromBundle;
40191
- return null;
40192
- }
40193
- function rewriteRuntimeSpecifier(importLine) {
40194
- const vendored = vendoredRuntimeClient();
40195
- if (!vendored) return importLine;
40196
- const m = importLine.match(/from\s+["'](@tw\/runtime)["']/);
40197
- if (!m) return importLine;
40198
- return importLine.replace(m[0], `from ${JSON.stringify(vendored)}`);
40199
- }
40200
- function buildClientChunk(rootDir, importLine, minify) {
40201
- if (!importLine) return "";
40202
- const cached = chunkCache.get(importLine);
40203
- if (cached && existsSync7(join11(rootDir, ".tw", cached))) return cached;
40204
- const jsDir = join11(rootDir, ".tw", "js");
40205
- mkdirSync4(jsDir, { recursive: true });
40206
- const names = importNames(importLine);
40207
- const entryLine = rewriteRuntimeSpecifier(importLine);
40208
- const entry = [
40209
- entryLine + ";",
40210
- "globalThis.__twClient = globalThis.__twClient || {};",
40211
- ...names.map((n) => "globalThis.__twClient." + n + " = " + n + ";")
40212
- ].join("\n");
40213
- const stamp = contentHash2(importLine);
40214
- const entryPath = join11(rootDir, ".tw", ".chunk-entry-" + stamp + ".js");
40215
- const tmpOut = join11(rootDir, ".tw", ".chunk-tmp-" + stamp + ".js");
40216
- writeFileSync4(entryPath, entry);
40217
- const bin = findEsbuildBin(rootDir);
40218
- const args = [entryPath, "--bundle", "--format=iife", "--target=es2019", "--outfile=" + tmpOut];
40219
- if (minify) args.push("--minify");
40220
- const r = spawnSync2(bin, args, { cwd: rootDir });
40221
- try {
40222
- unlinkSync2(entryPath);
40223
- } catch {
40224
- }
40225
- if (r.status !== 0) {
40226
- const errText = (r.stderr && r.stderr.toString ? r.stderr.toString() : "") || String(r.error || "unknown");
40227
- console.error(" [tw] client chunk failed (" + importLine + "): " + errText.split("\n")[0]);
40228
- try {
40229
- unlinkSync2(tmpOut);
40230
- } catch {
40231
- }
40232
- return "";
40233
- }
40234
- const code = readFileSync6(tmpOut, "utf-8");
40235
- const url = "js/c-" + contentHash2(code) + ".js";
40236
- writeFileSync4(join11(rootDir, ".tw", url), code);
40237
- try {
40238
- unlinkSync2(tmpOut);
40239
- } catch {
40240
- }
40241
- chunkCache.set(importLine, url);
40242
- return url;
40243
- }
40244
- function buildPageScope(rootDir, ownImportLines) {
40245
- if (!ownImportLines || ownImportLines.length === 0) return "";
40246
- const names = ownImportLines.flatMap((l) => importNames(l));
40247
- if (names.length === 0) return "";
40248
- mkdirSync4(join11(rootDir, ".tw", "js"), { recursive: true });
40249
- const body = names.map((n) => "p." + n + " = g.__twClient." + n + ";").join("\n");
40250
- const code = "(function(g){g.__twClient=g.__twClient||{};var p={};\n" + body + "\ng.__twClient.$page=p;})(globalThis);\n";
40251
- const url = "js/p-" + contentHash2(names.slice().sort().join("|")) + ".js";
40252
- writeFileSync4(join11(rootDir, ".tw", url), code);
40253
- return url;
40254
- }
40255
- var IMPORT_RE, BUILTIN_CLIENT_OK_SPECIFIERS, RUNTIME_CLIENT_OK_SPECIFIERS, chunkCache;
40256
- var init_client_bundle = __esm({
40257
- "tw/commands/client-bundle.ts"() {
40258
- IMPORT_RE = /^[ \t]*import\s+([^\n]+?)\s+from\s+["'']([^"'']+)["''][; \t]*$/gm;
40259
- BUILTIN_CLIENT_OK_SPECIFIERS = ["@tw/optImage", "@tw/RouterLink"];
40260
- RUNTIME_CLIENT_OK_SPECIFIERS = ["@tw/runtime"];
40261
- chunkCache = /* @__PURE__ */ new Map();
40262
- }
40263
- });
40264
-
40265
- // ../../packages/server/tw/routing/revalidate.ts
40266
- function setActivePipeline(p) {
40267
- activePipeline = p;
40268
- }
40269
- function getActivePipeline() {
40270
- return activePipeline;
40271
- }
40272
- function revalidatePath(pathname, opts) {
40273
- return activePipeline ? activePipeline.revalidatePath(pathname, opts) : 0;
40274
- }
40275
- function registerTwmTagInvalidator(fn) {
40276
- twmTagInvalidator = fn;
40277
- }
40278
- function revalidateTag(tag) {
40279
- let dropped = 0;
40280
- if (activePipeline) dropped += activePipeline.revalidateTag(tag);
40281
- if (twmTagInvalidator) dropped += twmTagInvalidator(tag);
40282
- return dropped;
40283
- }
40284
- var activePipeline, twmTagInvalidator;
40285
- var init_revalidate = __esm({
40286
- "../../packages/server/tw/routing/revalidate.ts"() {
40287
- activePipeline = null;
40288
- twmTagInvalidator = null;
40289
- }
40290
- });
40291
-
40292
- // ../../packages/server/tw/routing/signal-stream.ts
40293
- function getSignalHub() {
40294
- if (!hub) hub = new SignalHub();
40295
- return hub;
40296
- }
40297
- function setSignal(name, value, opts) {
40298
- let session = opts?.session;
40299
- if (!session && opts?.request) {
40300
- try {
40301
- const cookies = opts.request.cookies;
40302
- if (cookies && typeof cookies.get === "function") {
40303
- session = cookies.get("tw_session") ?? void 0;
40304
- } else {
40305
- const header = opts.request.headers?.get?.("cookie") ?? "";
40306
- const m = /(?:^|;\s*)tw_session=([^;]+)/.exec(header);
40307
- if (m) session = decodeURIComponent(m[1].trim().replace(/^"|"$/g, ""));
40308
- }
40309
- } catch {
40310
- }
40311
- }
40312
- return getSignalHub().setSignal(name, value, session ? { session } : void 0);
40313
- }
40314
- var BATCH_MS, HISTORY_LIMIT, SignalHub, hub;
40315
- var init_signal_stream = __esm({
40316
- "../../packages/server/tw/routing/signal-stream.ts"() {
40317
- BATCH_MS = 100;
40318
- HISTORY_LIMIT = 1e3;
40319
- SignalHub = class {
40320
- clients = /* @__PURE__ */ new Map();
40321
- nextClientId = 1;
40322
- seq = 0;
40323
- values = /* @__PURE__ */ new Map();
40324
- kinds = /* @__PURE__ */ new Map();
40325
- history = [];
40326
- pending = /* @__PURE__ */ new Map();
40327
- pendingSession;
40328
- timer = null;
40329
- /**
40330
- * Server-side mutation (route handlers): queue an update, batch-flush.
40331
- * With `session` (derived from the request's tw_session cookie) private
40332
- * signals are delivered only to clients authenticated as that session.
40333
- */
40334
- setSignal(name, value, opts) {
40335
- const kind = this.kinds.get(name) ?? "public";
40336
- this.values.set(name, value);
40337
- this.kinds.set(name, kind);
40338
- this.pending.set(name, value);
40339
- if (opts?.session) this.pendingSession = opts.session;
40340
- if (this.timer == null) {
40341
- this.timer = setTimeout(() => this.flush(), BATCH_MS);
40342
- }
40343
- return { queued: true, kind };
40344
- }
40345
- /** Latest known values (also used for fresh snapshots). */
40346
- snapshot() {
40347
- return { seq: this.seq, snapshot: Object.fromEntries(this.values) };
40348
- }
40349
- register(declared, session) {
40350
- const id = this.nextClientId++;
40351
- for (const [name, kind] of declared) {
40352
- if (!this.kinds.has(name)) this.kinds.set(name, kind);
40353
- }
40354
- const client = {
40355
- id,
40356
- declared,
40357
- session,
40358
- send: () => {
40359
- },
40360
- close: () => this.clients.delete(id)
40361
- };
40362
- this.clients.set(id, client);
40363
- return client;
40364
- }
40365
- /** Build the replay/snapshot preamble for a connecting client. */
40366
- connectPayloads(since, declared, session) {
40367
- const out = [];
40368
- const hasCoverage = since <= this.seq && this.history.length > 0 && since >= this.history[0].seq - 1;
40369
- if (hasCoverage) {
40370
- for (const entry of this.history) {
40371
- if (entry.seq > since && this.sessionMatches(session, entry) && this.deliversTo(declared, entry.names)) {
40372
- out.push(entry.payload);
40373
- }
40374
- }
40375
- } else if (this.values.size > 0) {
40376
- const snap = {};
40377
- for (const [name, value] of this.values) {
40378
- if (this.sessionMatches(session, { names: [name], session: this.valueSessions.get(name) }) && this.deliversTo(declared, [name])) snap[name] = value;
40379
- }
40380
- if (Object.keys(snap).length > 0) {
40381
- out.push(this.frame({ v: 1, seq: this.seq, snapshot: snap }));
40382
- }
40383
- }
40384
- return out;
40385
- }
40386
- /** latest-known session per signal (for snapshot filtering) */
40387
- valueSessions = /* @__PURE__ */ new Map();
40388
- sessionMatches(clientSession, entry) {
40389
- if (!entry.session) return true;
40390
- return entry.session === clientSession;
40391
- }
40392
- deliversTo(declared, names) {
40393
- return names.some((n) => {
40394
- const kind = this.kinds.get(n) ?? "public";
40395
- return kind === "public" || declared.has(n);
40396
- });
40397
- }
40398
- flush() {
40399
- this.timer = null;
40400
- if (this.pending.size === 0) return;
40401
- const updates = [...this.pending.entries()];
40402
- const session = this.pendingSession;
40403
- this.pending.clear();
40404
- this.pendingSession = void 0;
40405
- this.seq++;
40406
- const names = updates.map(([n]) => n);
40407
- for (const [n] of updates) this.valueSessions.set(n, session);
40408
- const payload = this.frame({ v: 1, seq: this.seq, updates });
40409
- this.history.push({ seq: this.seq, payload, names, session });
40410
- if (this.history.length > HISTORY_LIMIT) this.history.shift();
40411
- for (const client of this.clients.values()) {
40412
- if (this.sessionMatches(client.session, { names, session }) && this.deliversTo(client.declared, names)) client.send(payload);
40413
- }
40414
- }
40415
- frame(obj) {
40416
- return JSON.stringify(obj).replace(/</g, "\\u003c");
40417
- }
40418
- };
40419
- hub = null;
40420
- }
40421
- });
40422
-
40423
40664
  // ../../packages/server/tw/routing/twm-rules.ts
40424
40665
  var twm_rules_exports = {};
40425
40666
  __export(twm_rules_exports, {
@@ -40538,19 +40779,35 @@ function verifyJwt_hs256(token, secret) {
40538
40779
  return true;
40539
40780
  }
40540
40781
  function matchPath(pattern, pathname) {
40541
- if (pattern === "/**" || pattern === "/*") return true;
40542
- if (pattern.endsWith("/**")) {
40543
- const prefix = pattern.slice(0, -3);
40544
- return pathname === prefix || pathname.startsWith(prefix + "/");
40782
+ const norm = (p) => {
40783
+ let x = p;
40784
+ try {
40785
+ x = decodeURIComponent(x);
40786
+ } catch {
40787
+ }
40788
+ if (x.length > 1 && x.endsWith("/")) x = x.slice(0, -1);
40789
+ return x;
40790
+ };
40791
+ const pat = norm(pattern);
40792
+ const path = norm(pathname);
40793
+ if (pat === "/**") return true;
40794
+ if (pat === "/*") return path === "" || path === "/";
40795
+ if (pat.endsWith("/**")) {
40796
+ const prefix = pat.slice(0, -3);
40797
+ return path === prefix || path.startsWith(prefix + "/");
40545
40798
  }
40546
- if (pattern.endsWith("/*")) {
40547
- const prefix = pattern.slice(0, -2);
40548
- return pathname === prefix || pathname.startsWith(prefix + "/");
40799
+ if (pat.endsWith("/*")) {
40800
+ const prefix = pat.slice(0, -2);
40801
+ if (!path.startsWith(prefix + "/")) return path === prefix;
40802
+ const rest = path.slice(prefix.length + 1);
40803
+ return !rest.includes("/");
40549
40804
  }
40550
- return pathname === pattern;
40805
+ return path === pat;
40551
40806
  }
40552
40807
  function ruleBlocked(rule, request, pathname) {
40553
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;
40554
40811
  if (rule.userAgent) {
40555
40812
  const ua = (headers.get("user-agent") ?? "").toLowerCase();
40556
40813
  if (!ua) {
@@ -40870,6 +41127,631 @@ var init_twm_rules = __esm({
40870
41127
  }
40871
41128
  });
40872
41129
 
41130
+ // ../../packages/shared/tw/node-import.ts
41131
+ var node_import_exports = {};
41132
+ __export(node_import_exports, {
41133
+ twImportTs: () => twImportTs
41134
+ });
41135
+ import { join as join10 } from "node:path";
41136
+ import { tmpdir } from "node:os";
41137
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
41138
+ async function twImportTs(tsPath) {
41139
+ if (!globalThis.__TW_NODE) {
41140
+ return import(tsPath);
41141
+ }
41142
+ const esbuildMain = globalThis.__TW_ESBUILD_MODULE;
41143
+ const esbuild = await (esbuildMain ? import(esbuildMain) : import("esbuild"));
41144
+ const safe = tsPath.replace(/[^a-zA-Z0-9]/g, "_").slice(-120);
41145
+ const cachePath = join10(tmpdir(), "tw-" + safe + ".mjs");
41146
+ await esbuild.build({
41147
+ entryPoints: [tsPath],
41148
+ bundle: true,
41149
+ format: "esm",
41150
+ platform: "node",
41151
+ target: "node18",
41152
+ outfile: cachePath,
41153
+ logLevel: "silent"
41154
+ });
41155
+ return import(pathToFileURL2(cachePath).href);
41156
+ }
41157
+ var init_node_import = __esm({
41158
+ "../../packages/shared/tw/node-import.ts"() {
41159
+ }
41160
+ });
41161
+
41162
+ // tw/commands/client-bundle.ts
41163
+ var client_bundle_exports = {};
41164
+ __export(client_bundle_exports, {
41165
+ BUILTIN_CLIENT_OK_SPECIFIERS: () => BUILTIN_CLIENT_OK_SPECIFIERS,
41166
+ RUNTIME_CLIENT_OK_SPECIFIERS: () => RUNTIME_CLIENT_OK_SPECIFIERS,
41167
+ buildClientChunk: () => buildClientChunk,
41168
+ buildPageScope: () => buildPageScope,
41169
+ collectClientImportsFromSource: () => collectClientImportsFromSource,
41170
+ findServerOnlyViolations: () => findServerOnlyViolations,
41171
+ importNames: () => importNames,
41172
+ isClientImportSpecifier: () => isClientImportSpecifier
41173
+ });
41174
+ import { join as join11, resolve as resolve3 } from "node:path";
41175
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
41176
+ import { spawnSync as spawnSync2 } from "node:child_process";
41177
+ function isClientImportSpecifier(spec) {
41178
+ if (spec.startsWith("@./") || spec.startsWith("./") || spec.startsWith("../")) return false;
41179
+ if (/\.(tw|tss|twm|ts|js)$/.test(spec)) return false;
41180
+ if (spec.startsWith("@tw/")) {
41181
+ return RUNTIME_CLIENT_OK_SPECIFIERS.includes(spec);
41182
+ }
41183
+ return true;
41184
+ }
41185
+ function collectClientImportsFromSource(src) {
41186
+ const lines = [];
41187
+ let m;
41188
+ IMPORT_RE.lastIndex = 0;
41189
+ while (m = IMPORT_RE.exec(src)) {
41190
+ if (isClientImportSpecifier(m[2])) lines.push(m[0].trim().replace(/;$/, ""));
41191
+ }
41192
+ return lines;
41193
+ }
41194
+ function importNames(line) {
41195
+ const clause = line.match(/^import\s+([\s\S]+?)\s+from/);
41196
+ if (!clause) return [];
41197
+ const c = clause[1].trim();
41198
+ const names = [];
41199
+ if (c.startsWith("*")) {
41200
+ const as = c.split(/\s+as\s+/);
41201
+ if (as[1]) names.push(as[1].trim());
41202
+ } else if (c.startsWith("{")) {
41203
+ const braces = c.match(/\{([\s\S]*)\}/);
41204
+ if (braces) {
41205
+ for (const part of braces[1].split(",")) {
41206
+ const p = part.trim();
41207
+ if (!p) continue;
41208
+ const seg = p.split(/\s+as\s+/);
41209
+ names.push(seg[seg.length - 1].trim());
41210
+ }
41211
+ }
41212
+ } else {
41213
+ const def = c.match(/^[A-Za-z_$][\w$]*/);
41214
+ if (def) names.push(def[0]);
41215
+ }
41216
+ return names;
41217
+ }
41218
+ function findEsbuildBin(rootDir) {
41219
+ const candidates = [
41220
+ join11(rootDir, "node_modules", ".bin", "esbuild"),
41221
+ resolve3(rootDir, "..", "node_modules", ".bin", "esbuild"),
41222
+ // CLI repo layout (dev / source runs)
41223
+ resolve3(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "node_modules", ".bin", "esbuild")
41224
+ ];
41225
+ for (const c of candidates) {
41226
+ try {
41227
+ if (existsSync7(c)) return c;
41228
+ } catch {
41229
+ }
41230
+ }
41231
+ const bd = globalThis.__TW_BUNDLE_DIR;
41232
+ if (bd) {
41233
+ const b = resolve3(bd, "..", "..", "..", "node_modules", ".bin", "esbuild");
41234
+ try {
41235
+ if (existsSync7(b)) return b;
41236
+ } catch {
41237
+ }
41238
+ }
41239
+ return "esbuild";
41240
+ }
41241
+ function findServerOnlyViolations(src) {
41242
+ const out = [];
41243
+ let m;
41244
+ IMPORT_RE.lastIndex = 0;
41245
+ while (m = IMPORT_RE.exec(src)) {
41246
+ const spec = m[2];
41247
+ if (spec === "lib" || spec.startsWith("lib/")) {
41248
+ out.push(spec);
41249
+ continue;
41250
+ }
41251
+ if (isClientImportSpecifier(spec)) continue;
41252
+ if (spec.startsWith("@tw/")) {
41253
+ if (BUILTIN_CLIENT_OK_SPECIFIERS.includes(spec)) continue;
41254
+ if (RUNTIME_CLIENT_OK_SPECIFIERS.includes(spec)) continue;
41255
+ out.push(spec);
41256
+ continue;
41257
+ }
41258
+ if (spec.startsWith("@./") || spec.startsWith("./") || spec.startsWith("../")) {
41259
+ if (/\.(tw|tss)$/.test(spec)) continue;
41260
+ out.push(spec);
41261
+ continue;
41262
+ }
41263
+ if (spec.startsWith("lib/")) out.push(spec);
41264
+ }
41265
+ return out;
41266
+ }
41267
+ function contentHash2(s) {
41268
+ let h = 5381;
41269
+ for (let i = 0; i < s.length; i++) h = (h * 33 ^ s.charCodeAt(i)) >>> 0;
41270
+ return h.toString(36);
41271
+ }
41272
+ function vendoredRuntimeClient() {
41273
+ const fromSrc = new URL("../../dist/tw-runtime-client.mjs", import.meta.url).pathname;
41274
+ if (existsSync7(fromSrc)) return fromSrc;
41275
+ const fromBundle = new URL("./tw-runtime-client.mjs", import.meta.url).pathname;
41276
+ if (existsSync7(fromBundle)) return fromBundle;
41277
+ return null;
41278
+ }
41279
+ function rewriteRuntimeSpecifier(importLine) {
41280
+ const vendored = vendoredRuntimeClient();
41281
+ if (!vendored) return importLine;
41282
+ const m = importLine.match(/from\s+["'](@tw\/runtime)["']/);
41283
+ if (!m) return importLine;
41284
+ return importLine.replace(m[0], `from ${JSON.stringify(vendored)}`);
41285
+ }
41286
+ function buildClientChunk(rootDir, importLine, minify) {
41287
+ if (!importLine) return "";
41288
+ const cached = chunkCache.get(importLine);
41289
+ if (cached && existsSync7(join11(rootDir, ".tw", cached))) return cached;
41290
+ const jsDir = join11(rootDir, ".tw", "js");
41291
+ mkdirSync4(jsDir, { recursive: true });
41292
+ const names = importNames(importLine);
41293
+ const entryLine = rewriteRuntimeSpecifier(importLine);
41294
+ const entry = [
41295
+ entryLine + ";",
41296
+ "globalThis.__twClient = globalThis.__twClient || {};",
41297
+ ...names.map((n) => "globalThis.__twClient." + n + " = " + n + ";")
41298
+ ].join("\n");
41299
+ const stamp = contentHash2(importLine);
41300
+ const entryPath = join11(rootDir, ".tw", ".chunk-entry-" + stamp + ".js");
41301
+ const tmpOut = join11(rootDir, ".tw", ".chunk-tmp-" + stamp + ".js");
41302
+ writeFileSync4(entryPath, entry);
41303
+ const bin = findEsbuildBin(rootDir);
41304
+ const args = [entryPath, "--bundle", "--format=iife", "--target=es2019", "--outfile=" + tmpOut];
41305
+ if (minify) args.push("--minify");
41306
+ const r = spawnSync2(bin, args, { cwd: rootDir });
41307
+ try {
41308
+ unlinkSync2(entryPath);
41309
+ } catch {
41310
+ }
41311
+ if (r.status !== 0) {
41312
+ const errText = (r.stderr && r.stderr.toString ? r.stderr.toString() : "") || String(r.error || "unknown");
41313
+ console.error(" [tw] client chunk failed (" + importLine + "): " + errText.split("\n")[0]);
41314
+ try {
41315
+ unlinkSync2(tmpOut);
41316
+ } catch {
41317
+ }
41318
+ return "";
41319
+ }
41320
+ const code = readFileSync6(tmpOut, "utf-8");
41321
+ const url = "js/c-" + contentHash2(code) + ".js";
41322
+ writeFileSync4(join11(rootDir, ".tw", url), code);
41323
+ try {
41324
+ unlinkSync2(tmpOut);
41325
+ } catch {
41326
+ }
41327
+ chunkCache.set(importLine, url);
41328
+ return url;
41329
+ }
41330
+ function buildPageScope(rootDir, ownImportLines) {
41331
+ if (!ownImportLines || ownImportLines.length === 0) return "";
41332
+ const names = ownImportLines.flatMap((l) => importNames(l));
41333
+ if (names.length === 0) return "";
41334
+ mkdirSync4(join11(rootDir, ".tw", "js"), { recursive: true });
41335
+ const body = names.map((n) => "p." + n + " = g.__twClient." + n + ";").join("\n");
41336
+ const code = "(function(g){g.__twClient=g.__twClient||{};var p={};\n" + body + "\ng.__twClient.$page=p;})(globalThis);\n";
41337
+ const url = "js/p-" + contentHash2(names.slice().sort().join("|")) + ".js";
41338
+ writeFileSync4(join11(rootDir, ".tw", url), code);
41339
+ return url;
41340
+ }
41341
+ var IMPORT_RE, BUILTIN_CLIENT_OK_SPECIFIERS, RUNTIME_CLIENT_OK_SPECIFIERS, chunkCache;
41342
+ var init_client_bundle = __esm({
41343
+ "tw/commands/client-bundle.ts"() {
41344
+ IMPORT_RE = /^[ \t]*import\s+([^\n]+?)\s+from\s+["'']([^"'']+)["''][; \t]*$/gm;
41345
+ BUILTIN_CLIENT_OK_SPECIFIERS = ["@tw/optImage", "@tw/RouterLink"];
41346
+ RUNTIME_CLIENT_OK_SPECIFIERS = ["@tw/runtime"];
41347
+ chunkCache = /* @__PURE__ */ new Map();
41348
+ }
41349
+ });
41350
+
41351
+ // ../../packages/server/tw/routing/revalidate.ts
41352
+ function setActivePipeline(p) {
41353
+ activePipeline = p;
41354
+ }
41355
+ function getActivePipeline() {
41356
+ return activePipeline;
41357
+ }
41358
+ function revalidatePath(pathname, opts) {
41359
+ return activePipeline ? activePipeline.revalidatePath(pathname, opts) : 0;
41360
+ }
41361
+ function registerTwmTagInvalidator(fn) {
41362
+ twmTagInvalidator = fn;
41363
+ }
41364
+ function revalidateTag(tag) {
41365
+ let dropped = 0;
41366
+ if (activePipeline) dropped += activePipeline.revalidateTag(tag);
41367
+ if (twmTagInvalidator) dropped += twmTagInvalidator(tag);
41368
+ return dropped;
41369
+ }
41370
+ var activePipeline, twmTagInvalidator;
41371
+ var init_revalidate = __esm({
41372
+ "../../packages/server/tw/routing/revalidate.ts"() {
41373
+ activePipeline = null;
41374
+ twmTagInvalidator = null;
41375
+ }
41376
+ });
41377
+
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
+ });
41388
+ function getSignalHub() {
41389
+ if (!hub) hub = new SignalHub();
41390
+ return hub;
41391
+ }
41392
+ function setSignal(name, value, opts) {
41393
+ let session = opts?.session;
41394
+ if (!session && opts?.request) {
41395
+ try {
41396
+ const cookies = opts.request.cookies;
41397
+ if (cookies && typeof cookies.get === "function") {
41398
+ session = cookies.get("tw_session") ?? void 0;
41399
+ } else {
41400
+ const header = opts.request.headers?.get?.("cookie") ?? "";
41401
+ const m = /(?:^|;\s*)tw_session=([^;]+)/.exec(header);
41402
+ if (m) session = decodeURIComponent(m[1].trim().replace(/^"|"$/g, ""));
41403
+ }
41404
+ } catch {
41405
+ }
41406
+ }
41407
+ return getSignalHub().setSignal(name, value, session ? { session } : void 0);
41408
+ }
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;
41553
+ var init_signal_stream = __esm({
41554
+ "../../packages/server/tw/routing/signal-stream.ts"() {
41555
+ BATCH_MS = 100;
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}$/;
41561
+ SignalHub = class {
41562
+ clients = /* @__PURE__ */ new Map();
41563
+ nextClientId = 1;
41564
+ seq = 0;
41565
+ values = /* @__PURE__ */ new Map();
41566
+ kinds = /* @__PURE__ */ new Map();
41567
+ history = [];
41568
+ pending = /* @__PURE__ */ new Map();
41569
+ pendingSession;
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
+ }
41636
+ /**
41637
+ * Server-side mutation (route handlers): queue an update, batch-flush.
41638
+ * With `session` (derived from the request's tw_session cookie) private
41639
+ * signals are delivered only to clients authenticated as that session.
41640
+ */
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
+ }
41647
+ const kind = this.kinds.get(name) ?? "public";
41648
+ this.values.set(name, value);
41649
+ this.kinds.set(name, kind);
41650
+ this.pending.set(name, value);
41651
+ if (opts?.session) this.pendingSession = opts.session;
41652
+ if (this.timer == null) {
41653
+ this.timer = setTimeout(() => this.flush(), BATCH_MS);
41654
+ }
41655
+ return { queued: true, kind };
41656
+ }
41657
+ /** Latest known values (also used for fresh snapshots). */
41658
+ snapshot() {
41659
+ return { seq: this.seq, snapshot: Object.fromEntries(this.values) };
41660
+ }
41661
+ register(declared, session) {
41662
+ const id = this.nextClientId++;
41663
+ for (const [name, kind] of declared) {
41664
+ if (!this.kinds.has(name)) this.kinds.set(name, kind);
41665
+ }
41666
+ const client = {
41667
+ id,
41668
+ declared,
41669
+ session,
41670
+ send: () => {
41671
+ },
41672
+ close: () => this.clients.delete(id)
41673
+ };
41674
+ this.clients.set(id, client);
41675
+ return client;
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
+ }
41685
+ /** Build the replay/snapshot preamble for a connecting client. */
41686
+ connectPayloads(since, declared, session) {
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
+ }
41698
+ const hasCoverage = since <= this.seq && this.history.length > 0 && since >= this.history[0].seq - 1;
41699
+ if (hasCoverage) {
41700
+ for (const entry of this.history) {
41701
+ if (entry.seq > since && this.sessionMatches(session, entry) && this.deliversTo(declared, entry.names)) {
41702
+ out.push(entry.payload);
41703
+ }
41704
+ }
41705
+ } else if (this.values.size > 0) {
41706
+ const snap = {};
41707
+ for (const [name, value] of this.values) {
41708
+ if (this.sessionMatches(session, { names: [name], session: this.valueSessions.get(name) }) && this.deliversTo(declared, [name])) snap[name] = value;
41709
+ }
41710
+ if (Object.keys(snap).length > 0) {
41711
+ out.push(this.frame({ v: 1, seq: this.seq, snapshot: snap }));
41712
+ }
41713
+ }
41714
+ return out;
41715
+ }
41716
+ /** latest-known session per signal (for snapshot filtering) */
41717
+ valueSessions = /* @__PURE__ */ new Map();
41718
+ sessionMatches(clientSession, entry) {
41719
+ if (!entry.session) return true;
41720
+ return entry.session === clientSession;
41721
+ }
41722
+ deliversTo(declared, names) {
41723
+ return names.some((n) => {
41724
+ const kind = this.kinds.get(n) ?? "public";
41725
+ return kind === "public" || declared.has(n);
41726
+ });
41727
+ }
41728
+ flush() {
41729
+ this.timer = null;
41730
+ if (this.pending.size === 0) return;
41731
+ const updates = [...this.pending.entries()];
41732
+ const session = this.pendingSession;
41733
+ this.pending.clear();
41734
+ this.pendingSession = void 0;
41735
+ this.seq++;
41736
+ const names = updates.map(([n]) => n);
41737
+ for (const [n] of updates) this.valueSessions.set(n, session);
41738
+ const payload = this.frame({ v: 1, seq: this.seq, updates });
41739
+ this.history.push({ seq: this.seq, payload, names, session });
41740
+ this.totalUpdates += updates.length;
41741
+ this.batchesFlushed++;
41742
+ if (this.history.length > this.historyLimit) this.history.shift();
41743
+ for (const client of this.clients.values()) {
41744
+ if (this.sessionMatches(client.session, { names, session }) && this.deliversTo(client.declared, names)) client.send(payload);
41745
+ }
41746
+ }
41747
+ frame(obj) {
41748
+ return JSON.stringify(obj).replace(/</g, "\\u003c");
41749
+ }
41750
+ };
41751
+ hub = null;
41752
+ }
41753
+ });
41754
+
40873
41755
  // ../../packages/server/tw/websocket-manager.ts
40874
41756
  var DEFAULT_MAX_ROOMS, DEFAULT_MAX_ROOM_SIZE, WebSocketManager;
40875
41757
  var init_websocket_manager = __esm({
@@ -41096,7 +41978,8 @@ function extractRenderMode(page) {
41096
41978
  try {
41097
41979
  const fs = __require("node:fs");
41098
41980
  const source = fs.readFileSync(page.absolutePath, "utf-8");
41099
- const m = source.match(/render\s+(static|ssr|island|edge)/);
41981
+ const { maskSourceStringsAndComments: maskSourceStringsAndComments2 } = (init_tw(), __toCommonJS(tw_exports));
41982
+ const m = maskSourceStringsAndComments2(source).match(/render\s+(static|ssr|island|edge)/);
41100
41983
  if (m) {
41101
41984
  return m[1];
41102
41985
  }
@@ -41543,6 +42426,7 @@ var init_render_pipeline = __esm({
41543
42426
  init_twm_loader();
41544
42427
  init_scanner();
41545
42428
  init_layout_chain();
42429
+ init_signal_stream();
41546
42430
  init_helpers();
41547
42431
  init_tw6();
41548
42432
  init_twm_loader();
@@ -41696,6 +42580,17 @@ var init_render_pipeline = __esm({
41696
42580
  try {
41697
42581
  const pageState = { ...stateVars ?? {}, ...match.params ?? {}, params: match.params ?? {} };
41698
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
+ }
41699
42594
  const pageCompiled = this.compileFile(ctx.page, pageState);
41700
42595
  {
41701
42596
  const seed = pageCompiled.stateSeed;
@@ -41741,24 +42636,30 @@ var init_render_pipeline = __esm({
41741
42636
  let layoutTitle;
41742
42637
  const liftedParts = [];
41743
42638
  {
41744
- 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)) {
41745
42641
  const headInner = headMatch[1];
41746
42642
  const tMatch = headInner.match(/<title>([^<]*)<\/title>/i);
41747
- if (tMatch) layoutTitle = tMatch[1].trim();
41748
- 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();
41749
42646
  if (kids) liftedParts.push(kids);
41750
42647
  }
41751
- 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();
41752
42651
  bodyHTML = bodyHTML.replace(/<\/?(?:html|body)[^>]*>/gi, "");
41753
42652
  bodyHTML = bodyHTML.replace(/<!DOCTYPE[^>]*>/gi, "");
41754
42653
  }
41755
42654
  const liftedHead = liftedParts.join("\n");
41756
- if (ctx.error) {
42655
+ const liveRenderMode = this.extractRenderMode(ctx.page);
42656
+ if (ctx.error && (liveRenderMode === "ssr" || liveRenderMode === "stream")) {
41757
42657
  const errorCompiled = this.compileFile(ctx.error);
41758
42658
  allCSS = errorCompiled.css + "\n" + allCSS;
41759
42659
  bodyHTML = `<div data-error-boundary>${bodyHTML}</div>`;
41760
42660
  }
41761
- if (ctx.loading) {
42661
+ const needsSkeleton = bodyHTML.includes("data-tw-suspense") || bodyHTML.includes("data-tw-ppr");
42662
+ if (ctx.loading && needsSkeleton) {
41762
42663
  const loadingCompiled = this.compileFile(ctx.loading);
41763
42664
  allCSS = loadingCompiled.css + "\n" + allCSS;
41764
42665
  let lh = String(loadingCompiled.html || "");
@@ -41770,7 +42671,7 @@ ${bodyHTML}`;
41770
42671
  const pageTitle = this.extractTitle(ctx.page) || "TW Page";
41771
42672
  let title = pageTitle;
41772
42673
  if (layoutTitle !== void 0) {
41773
- title = layoutTitle.replace(/\{\s*page\.title\s*\}/g, pageTitle);
42674
+ title = layoutTitle.replace(/__TW_PAGETITLE__/g, pageTitle).replace(/\{\s*page\.title\s*\}/g, pageTitle);
41774
42675
  if (!title.trim()) title = pageTitle;
41775
42676
  }
41776
42677
  const metaTags = this.buildMetaTags(ctx.page);
@@ -41784,13 +42685,15 @@ ${bodyHTML}`;
41784
42685
  for (const hp of headCandidates) {
41785
42686
  const headCompiled = this.compileFile({ absolutePath: hp, type: "head" });
41786
42687
  const m = headCompiled.html.match(/<body>([\s\S]*)<\/body>/);
41787
- if (m && m[1].trim()) headExtra += m[1].trim() + "\n";
42688
+ if (m && m[1].trim()) headExtra += m[1].trim().replace(/\\([{}])/g, "$1") + "\n";
41788
42689
  }
41789
42690
  if (liftedHead) headExtra += liftedHead + "\n";
41790
42691
  } catch {
41791
42692
  }
41792
42693
  let fullHTML = this.assembleHTML(bodyHTML, allCSS, allJS, title, metaTags + headExtra);
41793
- 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)) {
41794
42697
  let stateSeed = "{}";
41795
42698
  try {
41796
42699
  stateSeed = JSON.stringify(pageState ?? {});
@@ -41909,7 +42812,8 @@ ${bodyHTML}`;
41909
42812
  throw new Error(`Route file not found: ${file.absolutePath}`);
41910
42813
  }
41911
42814
  const source = readFileSync7(file.absolutePath, "utf-8");
41912
- const result = compileSync(source, {
42815
+ const srcWithSentinel = source.replace(/\{\s*page\.title\s*\}/g, "__TW_PAGETITLE__");
42816
+ const result = compileSync(srcWithSentinel, {
41913
42817
  filePath: file.absolutePath,
41914
42818
  optimize: true,
41915
42819
  diagnostics: false,
@@ -41982,8 +42886,14 @@ ${bodyHTML}`;
41982
42886
  extractTitle(file) {
41983
42887
  try {
41984
42888
  const source = readFileSync7(file.absolutePath, "utf-8");
41985
- const m = source.match(/page\s*\{[^}]*title\s+"([^"]+)"/);
41986
- return m ? m[1] : null;
42889
+ const masked = maskSourceStringsAndComments(source);
42890
+ const m = masked.match(/page\s*\{[^}]*title\s/);
42891
+ if (!m || m.index === void 0) return null;
42892
+ const start = m.index + m[0].length;
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");
41987
42897
  } catch {
41988
42898
  return null;
41989
42899
  }
@@ -41997,7 +42907,9 @@ ${bodyHTML}`;
41997
42907
  extractRenderMode(file) {
41998
42908
  try {
41999
42909
  const src = readFileSync7(file.absolutePath, "utf8");
42000
- const m = /render\s+(static|ssr|island|edge|csr|stream|ppr)\b/.exec(src);
42910
+ const m = /render\s+(static|ssr|island|edge|csr|stream|ppr)\b/.exec(
42911
+ maskSourceStringsAndComments(src)
42912
+ );
42001
42913
  return m ? m[1] : "static";
42002
42914
  } catch {
42003
42915
  return "static";
@@ -42030,7 +42942,8 @@ ${bodyHTML}`;
42030
42942
  * render-cache never reads `stale`.
42031
42943
  */
42032
42944
  stampCacheHeaders(headers, entry, now, verdict) {
42033
- 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;
42034
42947
  const c = entry?.cache;
42035
42948
  if (c) {
42036
42949
  const ageSec = entry?.createdAt ? Math.max(0, Math.floor((now - entry.createdAt) / 1e3)) : 0;
@@ -42045,7 +42958,7 @@ ${bodyHTML}`;
42045
42958
  extractRevalidate(file) {
42046
42959
  try {
42047
42960
  const source = readFileSync7(file.absolutePath, "utf-8");
42048
- const m = source.match(/page\s*\{[^}]*revalidate\s+(\d+)/);
42961
+ const m = maskSourceStringsAndComments(source).match(/page\s*\{[^}]*revalidate\s+(\d+)/);
42049
42962
  if (m) {
42050
42963
  const n = Number(m[1]);
42051
42964
  return Number.isFinite(n) && n > 0 ? n : null;
@@ -42091,14 +43004,32 @@ ${bodyHTML}`;
42091
43004
  buildMetaTags(file) {
42092
43005
  try {
42093
43006
  const source = readFileSync7(file.absolutePath, "utf-8");
42094
- const fm = source.match(/page\s*\{([^}]*)\}/);
42095
- if (!fm) return "";
42096
- const body = fm[1];
43007
+ const masked = maskSourceStringsAndComments(source);
43008
+ const fm = masked.match(/page\s*\{/);
43009
+ if (!fm || fm.index === void 0) return "";
43010
+ const bodyStart = fm.index + fm[0].length;
43011
+ const bodyEnd = masked.indexOf("}", bodyStart);
43012
+ if (bodyEnd === -1) return "";
43013
+ const body = source.slice(bodyStart, bodyEnd);
42097
43014
  const AMP = String.fromCharCode(38);
42098
43015
  const esc = (v) => v.split(AMP).join(AMP + "amp;").split(String.fromCharCode(34)).join(AMP + "quot;").split("<").join(AMP + "lt;");
42099
43016
  const pick = (key) => {
42100
- const m = body.match(new RegExp(key + '\\s+"([^"]*)"'));
42101
- return m ? m[1] : null;
43017
+ const i = body.indexOf(key + ' "');
43018
+ if (i === -1) return null;
43019
+ const rest = body.slice(i + key.length + 2);
43020
+ let end = -1;
43021
+ for (let j = 0; j < rest.length; j++) {
43022
+ if (rest[j] === "\\") {
43023
+ j++;
43024
+ continue;
43025
+ }
43026
+ if (rest[j] === '"') {
43027
+ end = j;
43028
+ break;
43029
+ }
43030
+ }
43031
+ if (end === -1) return null;
43032
+ return rest.slice(0, end).replace(/\\([{}"])/g, "$1");
42102
43033
  };
42103
43034
  let tags = "";
42104
43035
  const description = pick("description");
@@ -43546,7 +44477,6 @@ function securityHeadersMiddleware() {
43546
44477
  return (req, res, next) => {
43547
44478
  res.headers.set("X-Content-Type-Options", "nosniff");
43548
44479
  res.headers.set("X-Frame-Options", "SAMEORIGIN");
43549
- res.headers.set("X-XSS-Protection", "1; mode=block");
43550
44480
  res.headers.set("X-Permitted-Cross-Domain-Policies", "none");
43551
44481
  res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
43552
44482
  res.headers.set("Cross-Origin-Opener-Policy", "same-origin");
@@ -43723,13 +44653,15 @@ function compressionMiddleware() {
43723
44653
  await next();
43724
44654
  };
43725
44655
  }
43726
- function securityHeadersMiddleware2() {
44656
+ function securityHeadersMiddleware2(opts) {
43727
44657
  return async (ctx, next) => {
43728
44658
  ctx.headers["x-content-type-options"] = "nosniff";
43729
44659
  ctx.headers["x-frame-options"] = "DENY";
43730
- ctx.headers["x-xss-protection"] = "1; mode=block";
43731
44660
  ctx.headers["referrer-policy"] = "strict-origin-when-cross-origin";
43732
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
+ }
43733
44665
  await next();
43734
44666
  };
43735
44667
  }
@@ -43812,6 +44744,161 @@ var init_middleware2 = __esm({
43812
44744
  }
43813
44745
  });
43814
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
+
43815
44902
  // ../../packages/server/tw/static/index.ts
43816
44903
  import { existsSync as existsSync11, statSync as statSync6 } from "node:fs";
43817
44904
  import { join as join15, extname as extname7, normalize as normalize2, resolve as resolve4, sep } from "node:path";
@@ -44021,7 +45108,7 @@ var init_policy = __esm({
44021
45108
  "Content-Security-Policy": this.build(),
44022
45109
  "X-Content-Type-Options": "nosniff",
44023
45110
  "X-Frame-Options": "DENY",
44024
- "X-XSS-Protection": "1; mode=block",
45111
+ // BUG 48: x-xss-protection removed (deprecated, harmful in old browsers)
44025
45112
  "Referrer-Policy": "strict-origin-when-cross-origin",
44026
45113
  "Permissions-Policy": "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()",
44027
45114
  "Cross-Origin-Opener-Policy": "same-origin",
@@ -48655,9 +49742,10 @@ function devSecurityHeaders() {
48655
49742
  removePoweredBy: true
48656
49743
  });
48657
49744
  }
48658
- var DEFAULTS2, SecurityHeaders2;
49745
+ var BASELINE_CSP, DEFAULTS2, SecurityHeaders2;
48659
49746
  var init_security_headers = __esm({
48660
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'";
48661
49749
  DEFAULTS2 = {
48662
49750
  hstsMaxAge: 31536e3,
48663
49751
  // 1 year
@@ -48666,7 +49754,9 @@ var init_security_headers = __esm({
48666
49754
  frameOptions: "SAMEORIGIN",
48667
49755
  contentTypeOptions: true,
48668
49756
  referrerPolicy: "strict-origin-when-cross-origin",
48669
- 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,
48670
49760
  removePoweredBy: true,
48671
49761
  coep: "unsafe-none",
48672
49762
  coop: "same-origin",
@@ -48722,6 +49812,9 @@ var init_security_headers = __esm({
48722
49812
  if (this.config.serverHeader) {
48723
49813
  headers["Server"] = this.config.serverHeader;
48724
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
+ }
48725
49818
  return headers;
48726
49819
  }
48727
49820
  /** Returns the security headers object. */
@@ -52932,161 +54025,6 @@ var init_ssr = __esm({
52932
54025
  }
52933
54026
  });
52934
54027
 
52935
- // ../../packages/server/tw/compression.ts
52936
- function shouldCompress(contentType, options = {}) {
52937
- const opts = { ...DEFAULT_OPTIONS4, ...options };
52938
- const ct = contentType.split(";")[0].trim();
52939
- return opts.contentTypes.includes(ct);
52940
- }
52941
- function isLargeEnough(size, options = {}) {
52942
- const opts = { ...DEFAULT_OPTIONS4, ...options };
52943
- return size >= opts.threshold;
52944
- }
52945
- async function compress(data, encoding = "gzip", options = {}) {
52946
- const opts = { ...DEFAULT_OPTIONS4, ...options };
52947
- const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
52948
- if (input.length < opts.threshold) {
52949
- return input;
52950
- }
52951
- try {
52952
- if (typeof Bun !== "undefined" && Bun.gzipSync) {
52953
- if (encoding === "gzip") return Bun.gzipSync(input, { level: opts.level });
52954
- if (encoding === "deflate" && Bun.deflateSync) return Bun.deflateSync(input, { level: opts.level });
52955
- }
52956
- } catch {
52957
- }
52958
- try {
52959
- const zlib2 = await import("node:zlib");
52960
- if (encoding === "gzip") {
52961
- return zlib2.gzipSync(input, { level: opts.level });
52962
- }
52963
- if (encoding === "deflate") {
52964
- return zlib2.deflateSync(input, { level: opts.level });
52965
- }
52966
- if (encoding === "brotli" && zlib2.brotliCompressSync) {
52967
- return zlib2.brotliCompressSync(input, {
52968
- params: {
52969
- [zlib2.constants.BROTLI_PARAM_QUALITY]: opts.level
52970
- }
52971
- });
52972
- }
52973
- } catch {
52974
- }
52975
- return input;
52976
- }
52977
- async function decompress(data, encoding = "gzip") {
52978
- try {
52979
- if (typeof Bun !== "undefined" && Bun.gunzipSync) {
52980
- if (encoding === "gzip") return Bun.gunzipSync(data);
52981
- if (encoding === "deflate" && Bun.inflateSync) return Bun.inflateSync(data);
52982
- }
52983
- } catch {
52984
- }
52985
- try {
52986
- const zlib2 = await import("node:zlib");
52987
- if (encoding === "gzip") return zlib2.gunzipSync(data);
52988
- if (encoding === "deflate") return zlib2.inflateSync(data);
52989
- if (encoding === "brotli" && zlib2.brotliDecompressSync) return zlib2.brotliDecompressSync(data);
52990
- } catch {
52991
- }
52992
- return data;
52993
- }
52994
- function negotiateEncoding(acceptEncoding) {
52995
- const encodings = acceptEncoding.split(",").map((e) => e.trim().split(";")[0].trim()).filter(Boolean);
52996
- if (encodings.includes("br")) return "brotli";
52997
- if (encodings.includes("gzip")) return "gzip";
52998
- if (encodings.includes("deflate")) return "deflate";
52999
- return null;
53000
- }
53001
- function createCompressionHeaders(encoding, originalSize, compressedSize) {
53002
- return {
53003
- "Content-Encoding": encoding === "brotli" ? "br" : encoding,
53004
- "Vary": "Accept-Encoding",
53005
- "X-Original-Size": String(originalSize),
53006
- "X-Compressed-Size": String(compressedSize),
53007
- "X-Compression-Ratio": originalSize > 0 ? `${((1 - compressedSize / originalSize) * 100).toFixed(1)}%` : "0%"
53008
- };
53009
- }
53010
- async function compressResponse(body, contentType, acceptEncoding, options = {}) {
53011
- const input = typeof body === "string" ? new TextEncoder().encode(body) : body;
53012
- const opts = { ...DEFAULT_OPTIONS4, ...options };
53013
- if (!shouldCompress(contentType, opts) || !isLargeEnough(input.length, opts)) {
53014
- return { body: input, headers: {}, compressed: false };
53015
- }
53016
- const encoding = negotiateEncoding(acceptEncoding);
53017
- if (!encoding) {
53018
- return { body: input, headers: {}, compressed: false };
53019
- }
53020
- try {
53021
- const compressed = await compress(input, encoding, opts);
53022
- const headers = createCompressionHeaders(encoding, input.length, compressed.length);
53023
- return { body: compressed, headers, compressed: true };
53024
- } catch {
53025
- return { body: input, headers: {}, compressed: false };
53026
- }
53027
- }
53028
- var DEFAULT_OPTIONS4, StreamingCompressor;
53029
- var init_compression = __esm({
53030
- "../../packages/server/tw/compression.ts"() {
53031
- DEFAULT_OPTIONS4 = {
53032
- level: 6,
53033
- threshold: 1024,
53034
- // Don't compress < 1KB
53035
- brotli: true,
53036
- contentTypes: [
53037
- "text/html",
53038
- "text/css",
53039
- "text/plain",
53040
- "text/javascript",
53041
- "application/javascript",
53042
- "application/json",
53043
- "application/xml",
53044
- "application/xhtml+xml",
53045
- "image/svg+xml",
53046
- "font/woff",
53047
- "font/woff2",
53048
- "application/wasm"
53049
- ]
53050
- };
53051
- StreamingCompressor = class {
53052
- chunks = [];
53053
- encoding;
53054
- options;
53055
- constructor(encoding = "gzip", options = {}) {
53056
- this.encoding = encoding;
53057
- this.options = { ...DEFAULT_OPTIONS4, ...options };
53058
- }
53059
- /**
53060
- * Add a chunk of data to be compressed.
53061
- */
53062
- push(data) {
53063
- const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
53064
- this.chunks.push(input);
53065
- }
53066
- /**
53067
- * Flush all data and return compressed result.
53068
- */
53069
- async flush() {
53070
- const totalLength = this.chunks.reduce((sum, c) => sum + c.length, 0);
53071
- const combined = new Uint8Array(totalLength);
53072
- let offset = 0;
53073
- for (const chunk of this.chunks) {
53074
- combined.set(chunk, offset);
53075
- offset += chunk.length;
53076
- }
53077
- this.chunks = [];
53078
- return compress(combined, this.encoding, this.options);
53079
- }
53080
- /**
53081
- * Get the total uncompressed size so far.
53082
- */
53083
- get uncompressedSize() {
53084
- return this.chunks.reduce((sum, c) => sum + c.length, 0);
53085
- }
53086
- };
53087
- }
53088
- });
53089
-
53090
54028
  // ../../packages/server/tw/edge.ts
53091
54029
  function createEdgeHandler(config) {
53092
54030
  const cache = /* @__PURE__ */ new Map();
@@ -53507,6 +54445,13 @@ function generateSecurityHeaders(options = {}) {
53507
54445
  const headers = {};
53508
54446
  if (opts.csp) {
53509
54447
  headers["Content-Security-Policy"] = buildCSPHeader(opts.csp);
54448
+ try {
54449
+ const scriptSrc = Array.isArray(opts.csp["script-src"]) ? opts.csp["script-src"].join(" ") : String(opts.csp["script-src"] ?? "");
54450
+ if (scriptSrc && !scriptSrc.includes("'unsafe-eval'") && !scriptSrc.includes("'strict-dynamic'")) {
54451
+ console.warn("[tw] security: configured CSP has no 'unsafe-eval' in script-src -- the island hydration runtime needs it (add script-src: ['self', ''unsafe-eval''] or migrate handlers).");
54452
+ }
54453
+ } catch {
54454
+ }
53510
54455
  }
53511
54456
  if (opts.hsts) {
53512
54457
  const hstsOpts = typeof opts.hsts === "object" ? opts.hsts : { maxAge: 31536e3, includeSubDomains: true };
@@ -53798,8 +54743,11 @@ var init_tw6 = __esm({
53798
54743
  init_tw2();
53799
54744
  init_websocket_manager();
53800
54745
  init_routing2();
54746
+ init_twm_rules();
53801
54747
  init_router();
53802
54748
  init_middleware2();
54749
+ init_compression();
54750
+ init_signal_stream();
53803
54751
  init_static();
53804
54752
  init_tw5();
53805
54753
  init_ssr();
@@ -53848,6 +54796,7 @@ var init_tw6 = __esm({
53848
54796
  }
53849
54797
  ssr = null;
53850
54798
  server = null;
54799
+ compressionMode = "off";
53851
54800
  isRunning = false;
53852
54801
  pluginManager = null;
53853
54802
  imageHandler = null;
@@ -53864,6 +54813,40 @@ var init_tw6 = __esm({
53864
54813
  const entries = Object.entries(cfgIn.redirects);
53865
54814
  cfgIn.redirects = entries.map(([from, to]) => typeof to === "string" ? { from, to } : { from, ...to });
53866
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
+ }
53867
54850
  if (cfgIn.headers && !Array.isArray(cfgIn.headers)) {
53868
54851
  const entries = Object.entries(cfgIn.headers);
53869
54852
  cfgIn.headers = entries.map(([source, headers]) => ({ source, headers }));
@@ -53881,6 +54864,12 @@ var init_tw6 = __esm({
53881
54864
  }
53882
54865
  }
53883
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
+ }
53884
54873
  try {
53885
54874
  this.imageHandler = createImageHandler({
53886
54875
  rootDir: this.options.rootDir,
@@ -53990,31 +54979,72 @@ var init_tw6 = __esm({
53990
54979
  if (existsSync13(middlewarePath)) {
53991
54980
  this.middlewarePath = middlewarePath;
53992
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
+ }
53993
54998
  }
53994
54999
  return this;
53995
55000
  }
53996
55001
  async start() {
53997
55002
  if (this.isRunning) return;
53998
- const { port, host } = this.options;
53999
- this.server = (typeof Bun !== "undefined" ? Bun : null)?.serve({
54000
- port,
54001
- hostname: host,
54002
- fetch: async (request) => {
54003
- const res = await this.handleRequest(request);
54004
- if (this.securityHeaders) {
54005
- try {
54006
- return this.securityHeaders.apply(res);
54007
- } catch {
54008
- 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 });
54009
55028
  }
54010
- }
54011
- return res;
54012
- },
54013
- error: (err3) => {
54014
- console.error("Server error:", err3);
54015
- 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}...`);
54016
55037
  }
54017
- });
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;
54018
55048
  this.isRunning = true;
54019
55049
  console.log(`
54020
55050
  TW Server running at http://${host}:${port}
@@ -54028,6 +55058,10 @@ var init_tw6 = __esm({
54028
55058
  // finishes ongoing requests, then closes
54029
55059
  async gracefulShutdown(timeout = 5e3) {
54030
55060
  this.isRunning = false;
55061
+ try {
55062
+ getSignalHub().closeAll();
55063
+ } catch {
55064
+ }
54031
55065
  if (this.server) {
54032
55066
  this.server.stop?.(false);
54033
55067
  const start = Date.now();
@@ -54053,6 +55087,35 @@ var init_tw6 = __esm({
54053
55087
  * - return { headers: {...} } -> headers merged into the final response
54054
55088
  * - return null/undefined -> continue
54055
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. */
54056
55119
  async handleRequest(request) {
54057
55120
  try {
54058
55121
  const u = new URL(request.url);
@@ -54225,69 +55288,12 @@ var init_tw6 = __esm({
54225
55288
  * `data: {"v":1,"seq":N,"updates":[...]}` frames.
54226
55289
  */
54227
55290
  handleSignalStream(request, url) {
54228
- const hub2 = getSignalHub();
54229
- const names = (url.searchParams.get("s") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
54230
- const since = Number(url.searchParams.get("since") ?? "0") || 0;
54231
- const declared = /* @__PURE__ */ new Map();
54232
- for (const n of names) declared.set(n, "public");
54233
- let session;
54234
- try {
54235
- const header = request.headers.get("cookie") ?? "";
54236
- const m = /(?:^|;\s*)tw_session=([^;]+)/.exec(header);
54237
- if (m) session = decodeURIComponent(m[1].trim().replace(/^"|"$/g, ""));
54238
- } catch {
54239
- }
54240
- const enc = new TextEncoder();
54241
- const sse = (payload) => enc.encode("data: " + payload + "\n\n");
54242
- const stream = new ReadableStream({
54243
- start(controller) {
54244
- const client = hub2.register(declared, session);
54245
- client.send = (payload) => {
54246
- try {
54247
- controller.enqueue(sse(payload));
54248
- } catch {
54249
- client.close();
54250
- }
54251
- };
54252
- client.close = () => {
54253
- try {
54254
- controller.close();
54255
- } catch {
54256
- }
54257
- hub2["clients"];
54258
- };
54259
- for (const payload of hub2.connectPayloads(since, declared, session)) {
54260
- try {
54261
- controller.enqueue(sse(payload));
54262
- } catch {
54263
- break;
54264
- }
54265
- }
54266
- const keepAlive = setInterval(() => {
54267
- try {
54268
- controller.enqueue(enc.encode(": keep-alive\n\n"));
54269
- } catch {
54270
- clearInterval(keepAlive);
54271
- }
54272
- }, 25e3);
54273
- client.close = () => {
54274
- clearInterval(keepAlive);
54275
- try {
54276
- controller.close();
54277
- } catch {
54278
- }
54279
- };
54280
- }
54281
- });
54282
- return new Response(stream, {
54283
- status: 200,
54284
- headers: {
54285
- "Content-Type": "text/event-stream; charset=utf-8",
54286
- "Cache-Control": "no-store",
54287
- "X-Accel-Buffering": "no",
54288
- Connection: "keep-alive"
54289
- }
54290
- });
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 });
54291
55297
  }
54292
55298
  /**
54293
55299
  * PPR hole fill: render the page fresh and return only the requested
@@ -54361,15 +55367,33 @@ var init_tw6 = __esm({
54361
55367
  controller.close();
54362
55368
  }
54363
55369
  });
54364
- return new Response(stream, {
55370
+ const streamRes = new Response(stream, {
54365
55371
  status: 200,
54366
55372
  headers: { "Content-Type": "text/html; charset=utf-8" }
54367
55373
  });
55374
+ streamRes.__twRawStream = true;
55375
+ return streamRes;
54368
55376
  }
54369
55377
  async handleRequestInner(request) {
54370
55378
  const url = new URL(request.url);
54371
55379
  const method = request.method;
54372
- 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;
54373
55397
  if (pathname === "/_tw/img" || pathname.startsWith("/_tw/img/")) {
54374
55398
  const imgRes = await this.imageHandler.handle(request);
54375
55399
  if (imgRes) return imgRes;
@@ -54377,6 +55401,9 @@ var init_tw6 = __esm({
54377
55401
  if (pathname === "/_tw/stream") {
54378
55402
  return this.handleSignalStream(request, url);
54379
55403
  }
55404
+ if (pathname === "/_tw/signal" && method === "POST") {
55405
+ return this.handleClientSignalWrite(request);
55406
+ }
54380
55407
  if (pathname === "/_tw/ppr") {
54381
55408
  return this.handlePPRHoleFill(url);
54382
55409
  }
@@ -54541,13 +55568,39 @@ __export(twm_loader_exports, {
54541
55568
  loadTWMModule: () => loadTWMModule,
54542
55569
  revalidateRoute: () => revalidateRoute,
54543
55570
  shouldMatchMiddleware: () => shouldMatchMiddleware,
54544
- stripCommentsSafe: () => stripCommentsSafe
55571
+ stripCommentsSafe: () => stripCommentsSafe,
55572
+ validateTWMSyntax: () => validateTWMSyntax
54545
55573
  });
54546
55574
  import { readFileSync as readFileSync10, existsSync as existsSync14 } from "node:fs";
54547
55575
  import { join as join18 } from "node:path";
54548
55576
  function clearTWMCache() {
54549
55577
  moduleCache.clear();
54550
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
+ }
54551
55604
  function stripCommentsSafe(src) {
54552
55605
  let out = "";
54553
55606
  let i = 0;
@@ -54785,7 +55838,7 @@ async function loadTWMModule(filePath, rootDir) {
54785
55838
  console.error("[tw] " + e.message + " (" + filePath + ":" + cm[1] + ")");
54786
55839
  }
54787
55840
  }
54788
- 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(", ") + " };";
54789
55842
  const injectedKeys = Object.keys(injected);
54790
55843
  let module = {};
54791
55844
  try {
@@ -54800,6 +55853,8 @@ async function loadTWMModule(filePath, rootDir) {
54800
55853
  put: H.put || H.PUT || void 0,
54801
55854
  delete: H.deleteFn || H.DELETE || void 0,
54802
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,
54803
55858
  middleware: H.middleware || void 0,
54804
55859
  generate: H.generate || void 0,
54805
55860
  config: H.config || void 0,
@@ -54931,9 +55986,26 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
54931
55986
  }
54932
55987
  } else if (typeof request?.json === "function") {
54933
55988
  try {
54934
- parsedBody = await request.json();
55989
+ const rawBody2 = await request.text();
55990
+ if (rawBody2.length > MAX_BODY_BYTES) {
55991
+ return { status: 413, json: { ok: false, error: "Payload too large" } };
55992
+ }
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
+ }
54935
56008
  } catch {
54936
- parsedBody = {};
54937
56009
  }
54938
56010
  } else if (request?.body && typeof request.body === "object") {
54939
56011
  parsedBody = request.body;
@@ -54941,7 +56013,18 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
54941
56013
  let query2 = {};
54942
56014
  try {
54943
56015
  const u = new URL(request.url);
54944
- 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
+ }
54945
56028
  } catch {
54946
56029
  }
54947
56030
  const headersObj = typeof request.headers?.entries === "function" ? Object.fromEntries(request.headers.entries()) : request.headers ?? {};
@@ -55070,9 +56153,17 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
55070
56153
  console.error("[tw] route handler error (" + filePath + "):", err3?.message ?? err3);
55071
56154
  return { status: 500, json: { ok: false, error: "Internal Server Error" } };
55072
56155
  }
55073
- if (!result || typeof result !== "object") {
55074
- console.error("[tw] route handler returned no result (" + filePath + ")");
55075
- 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
+ };
55076
56167
  }
55077
56168
  const finalResult = {
55078
56169
  status: typeof result.status === "number" ? result.status : 200,
@@ -55211,7 +56302,9 @@ function findCompilerPath2() {
55211
56302
  return resolve6(process.cwd(), "packages/compiler/tw/index.ts");
55212
56303
  }
55213
56304
  function extractRenderMode2(src) {
55214
- const m = /render\s+(static|ssr|island|edge|csr|stream|ppr|signalStream)\b/.exec(src);
56305
+ const m = /render\s+(static|ssr|island|edge|csr|stream|ppr|signalStream)\b/.exec(
56306
+ maskSourceStringsAndComments(src)
56307
+ );
55215
56308
  return m ? m[1] : "static";
55216
56309
  }
55217
56310
  function csrifyHtml(html) {
@@ -55325,18 +56418,32 @@ function scanComponents2(rootDir) {
55325
56418
  for (const d of privDirs) registerComponentsFrom2(d);
55326
56419
  }
55327
56420
  function buildMetaTagsFromSource(source) {
55328
- const fm = source.match(/page\s*\{([^}]*)\}/);
55329
- if (!fm) return "";
55330
- const body = fm[1];
56421
+ const masked = maskSourceStringsAndComments(source);
56422
+ const fm = masked.match(/page\s*\{/);
56423
+ if (!fm || fm.index === void 0) return "";
56424
+ const bodyStart = fm.index + fm[0].length;
56425
+ const bodyEnd = masked.indexOf("}", bodyStart);
56426
+ if (bodyEnd === -1) return "";
56427
+ const body = source.slice(bodyStart, bodyEnd);
55331
56428
  const AMP = String.fromCharCode(38);
55332
56429
  const esc = (v) => v.split(AMP).join(AMP + "amp;").split(String.fromCharCode(34)).join(AMP + "quot;").split("<").join(AMP + "lt;");
55333
56430
  const pick = (key) => {
55334
56431
  const i = body.indexOf(key + ' "');
55335
56432
  if (i === -1) return null;
55336
56433
  const rest = body.slice(i + key.length + 2);
55337
- const end = rest.indexOf('"');
56434
+ let end = -1;
56435
+ for (let j = 0; j < rest.length; j++) {
56436
+ if (rest[j] === "\\") {
56437
+ j++;
56438
+ continue;
56439
+ }
56440
+ if (rest[j] === '"') {
56441
+ end = j;
56442
+ break;
56443
+ }
56444
+ }
55338
56445
  if (end === -1) return null;
55339
- return rest.slice(0, end);
56446
+ return rest.slice(0, end).replace(/\\([{}"])/g, "$1");
55340
56447
  };
55341
56448
  let tags = "";
55342
56449
  const description = pick("description");
@@ -55411,11 +56518,18 @@ async function buildCommand() {
55411
56518
  } catch {
55412
56519
  }
55413
56520
  let pageCount = 0;
56521
+ let requestRenderedCount = 0;
55414
56522
  let apiCount = 0;
55415
56523
  let errorCount = 0;
55416
56524
  const isrRoutes = /* @__PURE__ */ new Map();
55417
56525
  const pageFiles = [];
55418
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
+ }
55419
56533
  begin("compile", pageFiles.length);
55420
56534
  const htmlOutputs = [];
55421
56535
  _compilerMod.clearCssCapture?.();
@@ -55433,11 +56547,20 @@ async function buildCommand() {
55433
56547
  const pmod = await loadTWMModule2(paramsTwm, rootDir);
55434
56548
  if (typeof pmod.generate === "function") {
55435
56549
  const arr = pmod.generate();
55436
- if (Array.isArray(arr) && arr.length > 0) paramSets = arr.map((x) => {
55437
- const o = {};
55438
- for (const [k, v] of Object.entries(x)) o[k] = String(v);
55439
- return o;
55440
- });
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
+ }
55441
56564
  }
55442
56565
  } catch (e) {
55443
56566
  console.log(" Warning: params.twm failed: " + e.message);
@@ -55466,7 +56589,7 @@ async function buildCommand() {
55466
56589
  errorCount++;
55467
56590
  }
55468
56591
  }
55469
- const revM = resolved ? null : /page\s*\{[^}]*revalidate\s+(\d+)/.exec(pageSrc);
56592
+ const revM = resolved ? null : /page\s*\{[^}]*revalidate\s+(\d+)/.exec(maskSourceStringsAndComments(pageSrc));
55470
56593
  if (resolved || revM) {
55471
56594
  for (const ps of paramSets) {
55472
56595
  const rp = routePath.split("/").map((seg) => {
@@ -55488,6 +56611,7 @@ async function buildCommand() {
55488
56611
  }
55489
56612
  const hasExplicitParams = paramSets.length > 1 || Object.keys(paramSets[0] ?? {}).length > 0;
55490
56613
  if ((renderMode === "ssr" || renderMode === "stream") && !hasExplicitParams) {
56614
+ requestRenderedCount++;
55491
56615
  console.log(" \x1B[36m~\x1B[0m " + routePath + " [" + renderMode + ": rendered at request time]");
55492
56616
  try {
55493
56617
  const src = readFileSync12(pageFile, "utf-8");
@@ -55498,6 +56622,29 @@ async function buildCommand() {
55498
56622
  for (const d of errs.slice(0, 5)) console.log(" " + d.line + ":" + d.col + " " + d.message);
55499
56623
  errorCount++;
55500
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
+ }
55501
56648
  } catch (e) {
55502
56649
  console.log(" \x1B[33m! " + routePath + ": " + (e && e.message ? e.message : e) + "\x1B[0m");
55503
56650
  errorCount++;
@@ -55516,7 +56663,12 @@ async function buildCommand() {
55516
56663
  const BUILTIN_ALIASES = /* @__PURE__ */ new Set(["Link", "RouterLink", "Image", "optImage", "Suspense"]);
55517
56664
  const unknown = [...used].filter((t2) => !BUILTIN_ALIASES.has(t2) && !registeredComponentNames.has(t2));
55518
56665
  if (unknown.length > 0) {
55519
- 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");
55520
56672
  process.exit(1);
55521
56673
  }
55522
56674
  }
@@ -55550,7 +56702,10 @@ async function buildCommand() {
55550
56702
  const headResult = _compileSync(headSource, { filePath: headSrc });
55551
56703
  const headBody = headResult.html.match(/<body>([\s\S]*)<\/body>/);
55552
56704
  if (headBody && headBody[1].trim()) {
55553
- html = html.replace("</head>", " " + headBody[1].trim() + "\n</head>");
56705
+ html = html.replace(
56706
+ "</head>",
56707
+ " " + headBody[1].trim().replace(/\\([{}])/g, "$1") + "\n</head>"
56708
+ );
55554
56709
  }
55555
56710
  } catch (e) {
55556
56711
  console.log(" Warning: head.tw failed: " + e.message);
@@ -55605,9 +56760,12 @@ async function buildCommand() {
55605
56760
  if (pu) clientTag += `<script defer src="/${pu}"></script>
55606
56761
  `;
55607
56762
  }
55608
- const runtimeTag = `<script defer src="/__tw_runtime.js"></script>`;
55609
- html = html.replace("</body>", " " + clientTag + runtimeTag + "\n</body>");
55610
- 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
+ }
55611
56769
  }
55612
56770
  if (renderMode === "csr") {
55613
56771
  html = csrifyHtml(html);
@@ -55662,6 +56820,14 @@ async function buildCommand() {
55662
56820
  } else {
55663
56821
  console.log(" \x1B[32mOK\x1B[0m " + displayPath + " -> " + (outRoute === "" ? ".tw/index.html" : ".tw/" + outRoute + "/index.html"));
55664
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
+ }
55665
56831
  pageCount++;
55666
56832
  } catch (err3) {
55667
56833
  console.log(" \x1B[31mX " + routePath + ": " + err3.message + "\x1B[0m");
@@ -55680,8 +56846,16 @@ async function buildCommand() {
55680
56846
  const outputDir = routeName === "" ? outDir : join20(outDir, routeName);
55681
56847
  mkdirSync6(outputDir, { recursive: true });
55682
56848
  copyFileSync2(twmFile, join20(outputDir, "route.twm"));
55683
- console.log(" \x1B[32mOK\x1B[0m /" + routeName + " [api]");
55684
- 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
+ }
55685
56859
  }
55686
56860
  if (existsSync16(libDir)) {
55687
56861
  copyDir2(libDir, join20(outDir, "lib"));
@@ -55760,7 +56934,8 @@ async function buildCommand() {
55760
56934
  }
55761
56935
  }
55762
56936
  console.log("\n \x1B[32mBuild complete!\x1B[0m");
55763
- console.log(" Pages: " + pageCount + " compiled");
56937
+ console.log(" Pages: " + pageCount + " compiled" + (requestRenderedCount > 0 ? " (+" + requestRenderedCount + " rendered at request time)" : ""));
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.");
55764
56939
  console.log(" APIs: " + apiCount + " routes");
55765
56940
  if (isrRoutes.size > 0) {
55766
56941
  const manifest = {};
@@ -57709,9 +58884,11 @@ var init_provider2 = __esm({
57709
58884
  getExpressionCompletions(doc) {
57710
58885
  const items = [];
57711
58886
  const definedVars = /* @__PURE__ */ new Set();
57712
- const scriptMatch = doc.content.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
58887
+ const scriptMatches = [...doc.content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi)];
58888
+ const scriptMatch = scriptMatches.length ? [scriptMatches.map((m) => m[1]).join("\n")] : null;
58889
+ const scriptContent = scriptMatch ? scriptMatch[0] : "";
57713
58890
  if (scriptMatch) {
57714
- const sc = scriptMatch[1];
58891
+ const sc = scriptContent;
57715
58892
  const varRegex = /(?:const|let|var)\s+(\w+)/g;
57716
58893
  let m;
57717
58894
  while ((m = varRegex.exec(sc)) !== null) {
@@ -58286,8 +59463,10 @@ var init_symbols = __esm({
58286
59463
  const doc = this.docManager.getDocument(uri);
58287
59464
  if (!doc) return [];
58288
59465
  const symbols = [];
58289
- const scriptMatch = doc.content.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
58290
- if (scriptMatch) symbols.push(...this.parseScriptSymbols(scriptMatch[1], this.findScriptStart(doc)));
59466
+ const scriptMatches = [...doc.content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi)];
59467
+ const scriptMatch = scriptMatches.length ? [scriptMatches.map((m) => m[1]).join("\n")] : null;
59468
+ const scriptContent = scriptMatch ? scriptMatch[0] : "";
59469
+ if (scriptMatch) symbols.push(...this.parseScriptSymbols(scriptContent, this.findScriptStart(doc)));
58291
59470
  symbols.push(...this.parseTemplateSymbols(doc));
58292
59471
  return symbols;
58293
59472
  }
@@ -58600,6 +59779,11 @@ var init_provider14 = __esm({
58600
59779
  });
58601
59780
 
58602
59781
  // ../../packages/lsp/tw/code-lens/provider.ts
59782
+ function stripLineComments(src) {
59783
+ return src.split("\n").map(function(l) {
59784
+ return l.replace(/(^|\s)\/\/.*$/, "");
59785
+ }).join("\n");
59786
+ }
58603
59787
  var CodeLensProvider;
58604
59788
  var init_provider15 = __esm({
58605
59789
  "../../packages/lsp/tw/code-lens/provider.ts"() {
@@ -58632,7 +59816,7 @@ var init_provider15 = __esm({
58632
59816
  const esc = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58633
59817
  const regex = new RegExp(`\\b${esc}\\b`, "g");
58634
59818
  for (const d of this.docManager.getAllDocuments()) {
58635
- const m = d.content.match(regex);
59819
+ const m = stripLineComments(d.content).match(regex);
58636
59820
  if (m) count += m.length - 1;
58637
59821
  }
58638
59822
  return Math.max(0, count);
@@ -58642,7 +59826,7 @@ var init_provider15 = __esm({
58642
59826
  const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58643
59827
  const regex = new RegExp(`<${esc}\\b`, "g");
58644
59828
  for (const d of this.docManager.getAllDocuments()) {
58645
- const m = d.content.match(regex);
59829
+ const m = stripLineComments(d.content).match(regex);
58646
59830
  if (m) count += m.length;
58647
59831
  }
58648
59832
  return count;
@@ -59854,15 +61038,37 @@ ${name}/
59854
61038
  async function createVercelJson(dir) {
59855
61039
  const config = {
59856
61040
  buildCommand: "npx tw build",
59857
- outputDirectory: ".tw",
59858
- installCommand: "bun install",
59859
- 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
+ ]
59860
61048
  };
59861
61049
  await writeFile(join(dir, "vercel.json"), JSON.stringify(config, null, 2));
59862
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");
59863
61069
  }
59864
61070
  async function createDefaultProject(dir, name) {
59865
- const T = (s) => s;
61071
+ const T = (s) => s.split("${name}").join(name);
59866
61072
  for (const d of [
59867
61073
  ["home"],
59868
61074
  ["home", "about"],
@@ -59885,8 +61091,6 @@ html {
59885
61091
  head {
59886
61092
  meta charset "utf-8"
59887
61093
  meta name "viewport" content "width=device-width, initial-scale=1"
59888
- meta name "description" content "{page.description}"
59889
- title "{page.title} -- ${"$"}{name}"
59890
61094
  }
59891
61095
  body {
59892
61096
  header.sitenav {
@@ -60222,6 +61426,10 @@ a:hover { td underline }
60222
61426
  // This sample blocks known bot user agents everywhere. Guards are
60223
61427
  // fail-closed: a matching request that fails the condition gets the
60224
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.
60225
61433
 
60226
61434
  rule "block bots" {
60227
61435
  match "/**"
@@ -60983,38 +62191,15 @@ function destroySession(req) {
60983
62191
  const match = cookie.match(new RegExp(`${SESSION_COOKIE}=([^;]+)`));
60984
62192
  if (match) sessions.delete(match[1]);
60985
62193
  }
60986
- function loadMiddleware(rootDir) {
60987
- const mwPath = join19(rootDir, "middleware.twm");
60988
- if (!existsSync15(mwPath)) return [];
60989
- const source = readFileSync11(mwPath, "utf-8");
60990
- const rules = [];
60991
- const ruleRegex = /rule\s+"([^"]+)"\s*{([^}]+)}/g;
60992
- let match;
60993
- while ((match = ruleRegex.exec(source)) !== null) {
60994
- const name = match[1];
60995
- const body = match[2];
60996
- const pathMatch = body.match(/match\s+"([^"]+)"/);
60997
- const authMatch = body.match(/auth\s+(true|false)/);
60998
- const redirectMatch = body.match(/redirect\s+"([^"]+)"/);
60999
- if (pathMatch) {
61000
- rules.push({
61001
- name,
61002
- match: pathMatch[1],
61003
- auth: authMatch ? authMatch[1] === "true" : false,
61004
- redirect: redirectMatch ? redirectMatch[1] : "/login"
61005
- });
61006
- }
61007
- }
61008
- return rules;
61009
- }
61010
- function matchGlob(path, pattern) {
61011
- if (pattern === "/**") return true;
61012
- if (pattern === path) return true;
61013
- if (pattern.endsWith("/**")) {
61014
- const prefix = pattern.slice(0, -3);
61015
- return path === prefix || path.startsWith(prefix + "/");
62194
+ async function loadMiddlewareRules(mwPath) {
62195
+ try {
62196
+ const source = readFileSync11(mwPath, "utf-8");
62197
+ const { parseRules: parseRules2 } = await Promise.resolve().then(() => (init_twm_rules(), twm_rules_exports));
62198
+ return parseRules2(source);
62199
+ } catch (e) {
62200
+ console.log(" Middleware error: " + e.message);
62201
+ return [];
61016
62202
  }
61017
- return false;
61018
62203
  }
61019
62204
  function get404() {
61020
62205
  return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>404 - Page Not Found</title><style>${DEFAULT_CSS}</style></head><body><div class="wrapper"><section class="hero"><div class="container"><h1>404</h1><p>Page not found</p><a class="btn" href="/">Go Home</a></div></section></div></body></html>`;
@@ -61093,7 +62278,8 @@ async function devCommand() {
61093
62278
  if (existsSync15(stylePath)) parts.push(readFileSync11(stylePath, "utf-8"));
61094
62279
  if (parts.length) css = parts.join("\n");
61095
62280
  }
61096
- const middlewareRules = loadMiddleware(rootDir);
62281
+ const mwPath = join19(rootDir, "middleware.twm");
62282
+ const middlewareRules = existsSync15(mwPath) ? await loadMiddlewareRules(mwPath) : [];
61097
62283
  scanComponents(rootDir);
61098
62284
  const compDir = join19(rootDir, "components");
61099
62285
  if (existsSync15(compDir)) {
@@ -61168,13 +62354,13 @@ async function devCommand() {
61168
62354
  }
61169
62355
  async function runUserMiddleware(req) {
61170
62356
  try {
61171
- const mwPath = join19(rootDir, "middleware.twm");
61172
- if (!existsSync15(mwPath)) return null;
62357
+ const mwPath2 = join19(rootDir, "middleware.twm");
62358
+ if (!existsSync15(mwPath2)) return null;
61173
62359
  const { executeMiddleware: executeMiddleware2, loadTWMModule: loadTWMModule2, shouldMatchMiddleware: shouldMatchMiddleware2 } = await Promise.resolve().then(() => (init_twm_loader(), twm_loader_exports));
61174
- const mod = await loadTWMModule2(mwPath, rootDir);
62360
+ const mod = await loadTWMModule2(mwPath2, rootDir);
61175
62361
  const mwPathname = new URL(req.url).pathname;
61176
62362
  if (!mod.config?.matcher || shouldMatchMiddleware2(mod.config.matcher, mwPathname)) {
61177
- const mwResult = await executeMiddleware2(mwPath, req, rootDir);
62363
+ const mwResult = await executeMiddleware2(mwPath2, req, rootDir);
61178
62364
  if (mwResult instanceof Response) return mwResult;
61179
62365
  if (mwResult && typeof mwResult === "object" && mwResult.headers && !mwResult.status && !mwResult.json) {
61180
62366
  return mwResult.headers;
@@ -61207,6 +62393,7 @@ async function devCommand() {
61207
62393
  }
61208
62394
  let devRedirects = [];
61209
62395
  let devCfgHeaders = [];
62396
+ let devSigCfg = void 0;
61210
62397
  try {
61211
62398
  const cfgFile = join19(rootDir, "tw.config.ts");
61212
62399
  if (existsSync15(cfgFile)) {
@@ -61219,27 +62406,38 @@ async function devCommand() {
61219
62406
  if (cfg?.headers) {
61220
62407
  devCfgHeaders = Array.isArray(cfg.headers) ? cfg.headers : Object.entries(cfg.headers).map(([source, headers]) => ({ source, headers }));
61221
62408
  }
62409
+ if (cfg?.signalStream) devSigCfg = cfg.signalStream;
61222
62410
  }
61223
62411
  } catch {
61224
62412
  }
61225
62413
  const devPathMatch = (pattern, pathname) => pattern === pathname || pattern.endsWith("/**") && pathname.startsWith(pattern.slice(0, -2) + "/") || pattern.endsWith("/") && pathname.startsWith(pattern) || pattern === "/**" && pathname.startsWith("/");
61226
62414
  async function devFetchInner(req) {
61227
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
+ }
61228
62427
  const path = url.pathname;
61229
62428
  const time = (/* @__PURE__ */ new Date()).toISOString().split("T")[1].split(".")[0];
61230
62429
  rescanComponentsIfNeeded(rootDir);
61231
62430
  console.log(" [" + time + "] " + req.method + " " + path);
61232
62431
  const session = getSession(req);
61233
- for (const rule of middlewareRules) {
61234
- if (rule.auth && matchGlob(path, rule.match)) {
61235
- if (!session || !session.userId) {
61236
- console.log(" -> 302 Redirect to " + rule.redirect + " (auth required)");
61237
- return new Response(null, {
61238
- status: 302,
61239
- headers: { Location: rule.redirect }
61240
- });
61241
- }
62432
+ try {
62433
+ const { evaluateRules: evaluateRules2 } = await Promise.resolve().then(() => (init_twm_rules(), twm_rules_exports));
62434
+ const mwRes = evaluateRules2(middlewareRules, req);
62435
+ if (mwRes) {
62436
+ console.log(" -> middleware response " + mwRes.status + " for " + path);
62437
+ return mwRes;
61242
62438
  }
62439
+ } catch (e) {
62440
+ console.log(" Middleware error: " + e.message);
61243
62441
  }
61244
62442
  if (path === "/__tw_runtime.js") {
61245
62443
  const _fu = import.meta.url.replace("file://", "");
@@ -61449,13 +62647,20 @@ async function devCommand() {
61449
62647
  if (/<\/body>/.test(html)) {
61450
62648
  html = html.replace("</body>", devClientTag + ' <script src="/__tw_runtime.js"></script>\n</body>');
61451
62649
  }
62650
+ const devGlobalHead = join19(homeDir, "head.tw");
62651
+ const devHeadSources = [];
62652
+ if (existsSync15(devGlobalHead)) devHeadSources.push(devGlobalHead);
61452
62653
  const headTw = join19(dirname5(resolved.file), "head.tw");
61453
- if (existsSync15(headTw)) {
62654
+ if (headTw !== devGlobalHead && existsSync15(headTw)) devHeadSources.push(headTw);
62655
+ for (const devHeadTw of devHeadSources) {
61454
62656
  try {
61455
- const headRes = compileSync3(readFileSync11(headTw, "utf-8"), { filePath: headTw });
62657
+ const headRes = compileSync3(readFileSync11(devHeadTw, "utf-8"), { filePath: devHeadTw });
61456
62658
  const headBody = headRes.html.match(/<body>([\s\S]*)<\/body>/);
61457
62659
  if (headBody && headBody[1].trim()) {
61458
- html = html.replace("</head>", " " + headBody[1].trim() + "\n</head>");
62660
+ html = html.replace(
62661
+ "</head>",
62662
+ " " + headBody[1].trim().replace(/\\([{}])/g, "$1") + "\n</head>"
62663
+ );
61459
62664
  }
61460
62665
  } catch (e) {
61461
62666
  console.log(" Warning: head.tw failed: " + e.message);
@@ -61498,56 +62703,108 @@ async function devCommand() {
61498
62703
  headers: { "Content-Type": "text/html" }
61499
62704
  });
61500
62705
  }
61501
- const server = Bun.serve({
61502
- port,
61503
- hostname: host,
61504
- async fetch(req) {
61505
- const reqUrl = new URL(req.url);
61506
- for (const r of devRedirects) {
61507
- if (!r?.from || !r?.to) continue;
61508
- if (devPathMatch(r.from, reqUrl.pathname)) {
61509
- const status = [301, 302, 307, 308].includes(r.status) ? r.status : 301;
61510
- let loc = r.to.startsWith("http") ? r.to : new URL(r.to, req.url).toString();
61511
- if (reqUrl.search && !loc.includes("?")) loc += reqUrl.search;
61512
- return new Response(null, { status, headers: { Location: loc } });
61513
- }
61514
- }
61515
- const mwHeaders = await runUserMiddleware(req);
61516
- if (mwHeaders instanceof Response) return mwHeaders;
61517
- const u = new URL(req.url);
61518
- if (u.pathname === "/_tw/img" || u.pathname.startsWith("/_tw/img/")) {
61519
- const imgRes = await devImageHandler?.handle(req);
61520
- if (imgRes) return imgRes;
61521
- }
61522
- const response = await devFetchInner(req);
61523
- if (mwHeaders && typeof mwHeaders === "object") {
61524
- for (const key of Object.keys(mwHeaders)) {
61525
- try {
61526
- if (!response.headers.has(key)) response.headers.set(key, mwHeaders[key]);
61527
- } catch {
61528
- }
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 {
61529
62741
  }
61530
62742
  }
61531
- const u2 = new URL(req.url);
61532
- for (const h of devCfgHeaders) {
61533
- if (h?.source && h?.headers && devPathMatch(h.source, u2.pathname)) {
61534
- for (const key of Object.keys(h.headers)) {
61535
- try {
61536
- if (!response.headers.has(key)) response.headers.set(key, String(h.headers[key]).replace(/[\r\n]/g, ""));
61537
- } catch {
61538
- }
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 {
61539
62751
  }
61540
62752
  }
61541
62753
  }
61542
- if (devSecurityHeaders2) {
61543
- try {
61544
- return devSecurityHeaders2.apply(response);
61545
- } catch {
61546
- }
62754
+ }
62755
+ if (devSecurityHeaders2) {
62756
+ try {
62757
+ return devSecurityHeaders2.apply(response);
62758
+ } catch {
61547
62759
  }
61548
- return response;
61549
62760
  }
61550
- });
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
+ }
61551
62808
  console.log(" Server running! Open http://" + host + ":" + port + " in your browser.");
61552
62809
  console.log(" Press Ctrl+C to stop.\n");
61553
62810
  }
@@ -61560,6 +62817,25 @@ import { join as join22 } from "node:path";
61560
62817
  import { existsSync as existsSync18 } from "node:fs";
61561
62818
  async function serveCommand() {
61562
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
+ }
61563
62839
  let port = null;
61564
62840
  let host = "0.0.0.0";
61565
62841
  for (let i = 0; i < args.length; i++) {
@@ -61574,8 +62850,8 @@ async function serveCommand() {
61574
62850
  const envPath = join22(rootDir, ".env");
61575
62851
  if (existsSync18(envPath)) {
61576
62852
  try {
61577
- const { readFileSync: readFileSync19 } = await import("node:fs");
61578
- 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")) {
61579
62855
  const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/.exec(line);
61580
62856
  if (m && !(m[1] in process.env)) {
61581
62857
  process.env[m[1]] = m[2].replace(/^["']|["']$/g, "");
@@ -61621,7 +62897,7 @@ async function serveCommand() {
61621
62897
  staticDir: outDir,
61622
62898
  config,
61623
62899
  ...config?.rateLimit ? { rateLimit: config.rateLimit } : {},
61624
- ...typeof config?.compression === "boolean" ? { compression: config.compression } : {},
62900
+ ...config?.compression !== void 0 || config?.server?.compression !== void 0 ? { compression: config?.compression ?? config?.server?.compression } : {},
61625
62901
  ...pluginManager && pluginManager.size() > 0 ? { plugins: pluginManager } : {},
61626
62902
  ...config?.images ? { images: config.images } : {},
61627
62903
  ...config?.security ? { security: config.security } : {}
@@ -61900,7 +63176,7 @@ async function adapterCommand() {
61900
63176
  // tw/commands/test.ts
61901
63177
  import { join as join27 } from "node:path";
61902
63178
  import { createRequire as _cr } from "node:module";
61903
- 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";
61904
63180
  var _require = _cr(import.meta.url);
61905
63181
  function collectTests(dir, out) {
61906
63182
  if (!existsSync22(dir)) return;
@@ -61956,6 +63232,17 @@ async function testCommand() {
61956
63232
  }
61957
63233
  }
61958
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
+ }
61959
63246
  if (files.length === 0) {
61960
63247
  console.log(" No tests found. Create tests/your-name.test.ts:");
61961
63248
  console.log(`
@@ -61987,8 +63274,9 @@ async function testCommand() {
61987
63274
  }
61988
63275
 
61989
63276
  // tw/commands/check.ts
61990
- import { join as join28 } from "node:path";
61991
- import { existsSync as existsSync23, readFileSync as readFileSync15, readdirSync as readdirSync10, statSync as statSync10 } from "node:fs";
63277
+ init_tw();
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";
61992
63280
  async function checkCommand() {
61993
63281
  const args = process.argv.slice(3);
61994
63282
  const rootDir = process.cwd();
@@ -62010,7 +63298,7 @@ async function checkCommand() {
62010
63298
  for (const filePath of twFiles) {
62011
63299
  const relativePath = filePath.replace(rootDir + "/", "");
62012
63300
  try {
62013
- const source = readFileSync15(filePath, "utf-8");
63301
+ const source = readFileSync16(filePath, "utf-8");
62014
63302
  const result = await compile2(source, {
62015
63303
  filePath,
62016
63304
  optimize: false,
@@ -62021,6 +63309,42 @@ async function checkCommand() {
62021
63309
  const errors = diagnostics.filter((d) => d.severity === "error");
62022
63310
  const warnings = diagnostics.filter((d) => d.severity === "warning");
62023
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
+ }
62024
63348
  totalErrors += errors.length;
62025
63349
  totalWarnings += warnings.length;
62026
63350
  totalInfo += info.length;
@@ -62043,26 +63367,55 @@ async function checkCommand() {
62043
63367
  console.debug(" Compiler not found, running basic checks...\n");
62044
63368
  for (const filePath of twFiles) {
62045
63369
  const relativePath = filePath.replace(rootDir + "/", "");
62046
- const source = readFileSync15(filePath, "utf-8");
62047
- const openBraces = (source.match(/\{/g) || []).length;
62048
- const closeBraces = (source.match(/\}/g) || []).length;
63370
+ const source = readFileSync16(filePath, "utf-8");
63371
+ const maskedSource = maskSourceStringsAndComments(source);
63372
+ const openBraces = (maskedSource.match(/\{/g) || []).length;
63373
+ const closeBraces = (maskedSource.match(/\}/g) || []).length;
62049
63374
  if (openBraces !== closeBraces) {
62050
63375
  totalErrors++;
62051
- console.debug(` X ${relativePath}: unbalanced braces (${openBraces} open, ${closeBraces} close)`);
63376
+ console.log(` X ${relativePath}: unbalanced braces (${openBraces} open, ${closeBraces} close)`);
62052
63377
  } else {
62053
- console.debug(` OK ${relativePath} -- syntax OK`);
63378
+ console.log(` OK ${relativePath} -- syntax OK`);
62054
63379
  }
62055
63380
  }
62056
63381
  }
62057
- console.debug(`
63382
+ console.log(`
62058
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
+ }
62059
63394
  if (totalErrors > 0) {
62060
- console.debug(" Status: FAILED\n");
63395
+ console.log(" Status: FAILED\n");
63396
+ process.exit(1);
63397
+ } else if (maxWarnings !== null && totalWarnings > maxWarnings) {
63398
+ console.log(` Status: FAILED -- ${totalWarnings} warnings exceed --max-warnings=${maxWarnings}
63399
+ `);
62061
63400
  process.exit(1);
62062
63401
  } else {
62063
- console.debug(" Status: PASSED\n");
63402
+ console.log(" Status: PASSED\n");
62064
63403
  }
62065
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
+ }
62066
63419
  function collectFiles2(dir, ext, results) {
62067
63420
  const entries = readdirSync10(dir);
62068
63421
  for (const entry of entries) {
@@ -62077,11 +63430,12 @@ function collectFiles2(dir, ext, results) {
62077
63430
  }
62078
63431
 
62079
63432
  // tw/commands/plugin.ts
63433
+ init_tw();
62080
63434
  import { join as join29 } from "node:path";
62081
- 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";
62082
63436
  function readConfigPlugins(cfgPath) {
62083
63437
  if (!existsSync24(cfgPath)) return [];
62084
- const src = readFileSync16(cfgPath, "utf-8");
63438
+ const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
62085
63439
  const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
62086
63440
  if (!m) return [];
62087
63441
  const out = [];
@@ -62100,7 +63454,7 @@ function addConfigPlugin(cfgPath, name) {
62100
63454
  `);
62101
63455
  return true;
62102
63456
  }
62103
- const src = readFileSync16(cfgPath, "utf-8");
63457
+ const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
62104
63458
  if (new RegExp(`["'\`]${name}["'\`]`).test(src)) return false;
62105
63459
  const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
62106
63460
  if (m) {
@@ -62117,7 +63471,7 @@ function addConfigPlugin(cfgPath, name) {
62117
63471
  }
62118
63472
  function removeConfigPlugin(cfgPath, name) {
62119
63473
  if (!existsSync24(cfgPath)) return false;
62120
- const src = readFileSync16(cfgPath, "utf-8");
63474
+ const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
62121
63475
  const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
62122
63476
  if (!m) return false;
62123
63477
  const kept = m[1].split(",").map((e) => e.trim()).filter((e) => e && e !== `"${name}"` && e !== `'${name}'` && e !== "`" + name + "`");
@@ -62321,13 +63675,13 @@ async function lspCommand() {
62321
63675
  }
62322
63676
 
62323
63677
  // tw/help.ts
62324
- import { readFileSync as readFileSync17 } from "node:fs";
63678
+ import { readFileSync as readFileSync18 } from "node:fs";
62325
63679
  import { join as join31 } from "node:path";
62326
63680
  function cliVersion() {
62327
63681
  try {
62328
63682
  const bd = globalThis.__TW_BUNDLE_DIR;
62329
63683
  const pj = bd ? join31(bd, "..", "package.json") : new URL("../../package.json", import.meta.url).pathname;
62330
- return JSON.parse(readFileSync17(pj, "utf-8")).version ?? "0.0.0";
63684
+ return JSON.parse(readFileSync18(pj, "utf-8")).version ?? "0.0.0";
62331
63685
  } catch {
62332
63686
  return "0.0.0";
62333
63687
  }
@@ -62400,13 +63754,13 @@ var helpText = `
62400
63754
 
62401
63755
  // tw/commands/index.ts
62402
63756
  await init_build();
62403
- import { readFileSync as readFileSync18 } from "node:fs";
63757
+ import { readFileSync as readFileSync19 } from "node:fs";
62404
63758
  import { join as join32 } from "node:path";
62405
63759
  var VERSION = (() => {
62406
63760
  try {
62407
63761
  const bd = globalThis.__TW_BUNDLE_DIR;
62408
63762
  const pj = bd ? join32(bd, "..", "package.json") : new URL("../../package.json", import.meta.url).pathname;
62409
- return JSON.parse(readFileSync18(pj, "utf-8")).version ?? "0.0.0";
63763
+ return JSON.parse(readFileSync19(pj, "utf-8")).version ?? "0.0.0";
62410
63764
  } catch {
62411
63765
  return "0.0.0";
62412
63766
  }