tw-framework 1.0.6 → 1.0.7

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();
@@ -13260,6 +13374,7 @@ function consumeStateValue(cursor) {
13260
13374
  if (!t2) return "";
13261
13375
  if (t2.type === "STRING" || t2.type === "NUMBER" || t2.type === "IDENT" || t2.type === "KEYWORD") {
13262
13376
  cursor.advance();
13377
+ if (t2.type === "STRING") return String(t2.value).replace(/\\([{}"])/g, "$1");
13263
13378
  return t2.value;
13264
13379
  }
13265
13380
  if (t2.type === "LBRACKET" || t2.type === "LBRACE" || t2.type === "LBRACK") {
@@ -13272,7 +13387,7 @@ function consumeStateValue(cursor) {
13272
13387
  else if (tk.type === "RBRACKET" || tk.type === "RBRACE" || tk.type === "RBRACK") depth--;
13273
13388
  const after = cursor.peek(1);
13274
13389
  if (tk.type === "STRING") {
13275
- parts.push(`"${tk.value}"`);
13390
+ parts.push(`"${String(tk.value).replace(/\\([{}"])/g, "$1")}"`);
13276
13391
  } else if (tk.type === "IDENT" && after && after.type === "COLON" && depth > 0) {
13277
13392
  parts.push(`"${tk.value}"`);
13278
13393
  } else {
@@ -21274,6 +21389,15 @@ function splitArgs2(str) {
21274
21389
  function findMatchingBrace3(str, start) {
21275
21390
  let depth = 1;
21276
21391
  for (let i = start + 1; i < str.length; i++) {
21392
+ if (str[i] === '"' || str[i] === "'") {
21393
+ const q = str[i];
21394
+ i++;
21395
+ while (i < str.length && str[i] !== q) {
21396
+ if (str[i] === "\\") i++;
21397
+ i++;
21398
+ }
21399
+ continue;
21400
+ }
21277
21401
  if (str[i] === "{") depth++;
21278
21402
  else if (str[i] === "}") {
21279
21403
  depth--;
@@ -22826,7 +22950,7 @@ var init_types4 = __esm({
22826
22950
  var TW_VERSION, TW_GENERATOR_META;
22827
22951
  var init_version = __esm({
22828
22952
  "../../packages/compiler/tw/codegen/version.ts"() {
22829
- TW_VERSION = "1.0.6";
22953
+ TW_VERSION = "1.0.7";
22830
22954
  TW_GENERATOR_META = `<meta name="generator" content="TW Framework ${TW_VERSION}">`;
22831
22955
  }
22832
22956
  });
@@ -23386,6 +23510,14 @@ function interpolate(template, vars) {
23386
23510
  let i = 0;
23387
23511
  while (i < template.length) {
23388
23512
  const ch = template[i];
23513
+ if (ch === "\\") {
23514
+ const nxt = template[i + 1];
23515
+ if (nxt === "{" || nxt === "}") {
23516
+ result += nxt;
23517
+ i += 2;
23518
+ continue;
23519
+ }
23520
+ }
23389
23521
  if (ch === "{" && template[i + 1] !== "{") {
23390
23522
  let depth = 1;
23391
23523
  let j = i + 1;
@@ -23874,7 +24006,8 @@ function validatePlainCss(src, filePath) {
23874
24006
  function compileDecl(decl) {
23875
24007
  const trimmed = decl.trim();
23876
24008
  if (!trimmed) return "";
23877
- if (trimmed.includes(":")) return trimmed.replace(/\s*:\s*/, ": ");
24009
+ const cssForm = /^([-\w]+)\s*:\s*/.exec(trimmed);
24010
+ if (cssForm) return trimmed.replace(/^([-\w]+)\s*:\s*/, "$1: ");
23878
24011
  const spaceIdx = trimmed.search(/[\s]/);
23879
24012
  if (spaceIdx === -1) {
23880
24013
  return TSS_SHORTHANDS2[trimmed] ?? trimmed;
@@ -23888,9 +24021,72 @@ function combineSelector(parent, child) {
23888
24021
  if (child.includes("&")) return child.replace(/&/g, parent);
23889
24022
  return `${parent} ${child}`;
23890
24023
  }
24024
+ function stripTssComments(src) {
24025
+ let out = "";
24026
+ let i = 0;
24027
+ const n = src.length;
24028
+ while (i < n) {
24029
+ const c = src[i];
24030
+ if (c === '"' || c === "'") {
24031
+ const q = c;
24032
+ out += c;
24033
+ i++;
24034
+ while (i < n && src[i] !== q) {
24035
+ if (src[i] === "\\") {
24036
+ out += src[i++];
24037
+ if (i < n) out += src[i++];
24038
+ continue;
24039
+ }
24040
+ out += src[i++];
24041
+ }
24042
+ out += src[i] ?? "";
24043
+ i++;
24044
+ continue;
24045
+ }
24046
+ if (c === "u" && /^url\s*\(/i.test(src.slice(i, i + 6))) {
24047
+ let depth = 0;
24048
+ let j = i;
24049
+ for (; j < n; j++) {
24050
+ if (src[j] === "(") depth++;
24051
+ else if (src[j] === ")") {
24052
+ depth--;
24053
+ if (depth === 0) {
24054
+ j++;
24055
+ break;
24056
+ }
24057
+ }
24058
+ }
24059
+ out += src.slice(i, j);
24060
+ i = j;
24061
+ continue;
24062
+ }
24063
+ if (c === "/" && src[i + 1] === "/") {
24064
+ while (i < n && src[i] !== "\n") i++;
24065
+ continue;
24066
+ }
24067
+ if (c === "/" && src[i + 1] === "*") {
24068
+ const end = src.indexOf("*/", i + 2);
24069
+ i = end === -1 ? n : end + 2;
24070
+ out += " ";
24071
+ continue;
24072
+ }
24073
+ out += c;
24074
+ i++;
24075
+ }
24076
+ return out;
24077
+ }
23891
24078
  function compileTSS(source) {
24079
+ const __twOut = __twCompileTSSInner(source);
24080
+ const leftover = __twOut.match(/\$[A-Za-z_][\w-]*/g);
24081
+ if (leftover && !/\$\{/.test(__twOut)) {
24082
+ const seen = Array.from(new Set(leftover));
24083
+ console.warn("[tss] unsubstituted $var(s) in output: " + seen.join(", ") + " -- define them as state vars or fix the spelling");
24084
+ }
24085
+ return __twOut;
24086
+ }
24087
+ function __twCompileTSSInner(source) {
23892
24088
  if (!source || !source.trim()) return "";
23893
- let src = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
24089
+ let src = stripTssComments(source);
23894
24090
  const vars = {};
23895
24091
  src = src.replace(/\$([A-Za-z_][\w-]*)\s*:\s*([^;{}\n]+);?/g, (_m, name, val) => {
23896
24092
  vars[name] = String(val).trim();
@@ -24100,6 +24296,11 @@ function stripComments(src) {
24100
24296
  out += c;
24101
24297
  i++;
24102
24298
  while (i < n && src[i] !== q) {
24299
+ if (src[i] === "\\") {
24300
+ out += src[i++];
24301
+ if (i < n) out += src[i++];
24302
+ continue;
24303
+ }
24103
24304
  out += src[i++];
24104
24305
  }
24105
24306
  out += src[i] ?? "";
@@ -24418,9 +24619,19 @@ function emit(items, parent, env, mixins, out) {
24418
24619
  out.push(...childCss);
24419
24620
  }
24420
24621
  function compileSCSS(source) {
24622
+ const cleaned0 = stripComments(source);
24623
+ const UNSUPPORTED = /@(import|mixin|include|extend|use|forward|function|each|if)\b/g;
24624
+ const m = cleaned0.match(UNSUPPORTED);
24625
+ if (m) {
24626
+ const seen = Array.from(new Set(m.map((x) => x.trim())));
24627
+ console.warn("[scss] unsupported at-rule(s) ignored: " + seen.join(", ") + " -- TW's SCSS subset supports nesting, $vars and & references only");
24628
+ }
24421
24629
  const items = parseItems(stripComments(source));
24422
24630
  const out = [];
24423
24631
  emit(items, "", [/* @__PURE__ */ new Map()], /* @__PURE__ */ new Map(), out);
24632
+ if (out.length === 0 && cleaned0.replace(/[\s;]/g, "").length > 0) {
24633
+ console.warn("[scss] compileSCSS produced no output for a non-empty source -- check for unsupported syntax (TW uses `color: #fff` declarations, not `color #fff`)");
24634
+ }
24424
24635
  return out.join("\n");
24425
24636
  }
24426
24637
  var init_scss = __esm({
@@ -24460,7 +24671,8 @@ function addScope(css, scopeId) {
24460
24671
  return css.replace(/([.#]?[\w-]+)\s*\{/g, `$1[data-tw-scope="${scopeId}"] {`);
24461
24672
  }
24462
24673
  function interpolate2(expr, vars) {
24463
- return expr.replace(/\{([^}]+)\}/g, (_, name) => {
24674
+ const masked = expr.replace(/\\\{/g, "O").replace(/\\\}/g, "C");
24675
+ const out = masked.replace(/\{([^}]+)\}/g, (_, name) => {
24464
24676
  const key = name.trim();
24465
24677
  if (/^[a-zA-Z_$][\w$]*$/.test(key)) {
24466
24678
  return vars[key] ?? "";
@@ -24468,6 +24680,7 @@ function interpolate2(expr, vars) {
24468
24680
  const val = evaluate(key, vars);
24469
24681
  return val === null ? "" : val;
24470
24682
  });
24683
+ return out.replace(/\u0001O/g, "{").replace(/\u0001C/g, "}");
24471
24684
  }
24472
24685
  function isTruthy2(val) {
24473
24686
  return isTruthy(val);
@@ -24869,6 +25082,8 @@ function renderAttrs(el, ctx) {
24869
25082
  } else if (typeof attr.value === "string" && !attr.name.startsWith("data-tw-event") && /\{[^}]+\}/.test(attr.value) && Object.keys(ctx.stateVars).length > 0) {
24870
25083
  const val = interpolate2(attr.value, ctx.stateVars);
24871
25084
  parts.push(`${attr.name}="${escapeAttr2(sanitizeUrlAttr(attr.name, val))}"`);
25085
+ } else if (typeof attr.value === "string" && /\\[{}]/.test(attr.value)) {
25086
+ parts.push(`${attr.name}="${escapeAttr2(sanitizeUrlAttr(attr.name, String(attr.value).replace(/\\([{}])/g, "$1")))}"`);
24872
25087
  } else {
24873
25088
  parts.push(`${attr.name}="${escapeAttr2(sanitizeUrlAttr(attr.name, attr.value))}"`);
24874
25089
  }
@@ -24975,6 +25190,9 @@ function generateText(text, ctx) {
24975
25190
  }
24976
25191
  return escapeHTML(interpolate2(text.value, ctx.stateVars));
24977
25192
  }
25193
+ if (text.value && /\\[{}]/.test(text.value)) {
25194
+ return escapeHTML(text.value.replace(/\\([{}])/g, "$1"));
25195
+ }
24978
25196
  return escapeHTML(text.value);
24979
25197
  }
24980
25198
  function registerComponentTemplate(name, program) {
@@ -26458,7 +26676,7 @@ function emitElement(gen, el, ctx) {
26458
26676
  attrStr = el.attrs.map((a) => {
26459
26677
  if (a.value === true) return a.name;
26460
26678
  const val = a.isInterpolated ? interpolate3(a.value, ctx.stateVars) : a.value;
26461
- return `${a.name}="${escapeAttr3(val)}"`;
26679
+ return `${a.name}="${escapeAttr3(sanitizeUrlAttr2(a.name, val))}"`;
26462
26680
  }).join(" ");
26463
26681
  }
26464
26682
  if (el.styles && el.styles.length > 0) {
@@ -26574,14 +26792,25 @@ function getDirectiveValue2(program, key) {
26574
26792
  function escapeHTML2(str) {
26575
26793
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
26576
26794
  }
26795
+ function sanitizeUrlAttr2(name, value) {
26796
+ const URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "poster", "background"]);
26797
+ if (!URL_ATTRS.has(name.toLowerCase())) return value;
26798
+ const trimmed = String(value).trim().toLowerCase().replace(/[\s\0]/g, "");
26799
+ if (trimmed.startsWith("javascript:") || trimmed.startsWith("data:text/html") || trimmed.startsWith("vbscript:")) {
26800
+ return "#";
26801
+ }
26802
+ return value;
26803
+ }
26577
26804
  function escapeAttr3(str) {
26578
26805
  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
26579
26806
  }
26580
26807
  function interpolate3(expr, vars) {
26581
- return expr.replace(/\{([^}]+)\}/g, (_, name) => {
26808
+ const masked = expr.replace(/\\\{/g, "O").replace(/\\\}/g, "C");
26809
+ const out = masked.replace(/\{([^}]+)\}/g, (_, name) => {
26582
26810
  const key = name.trim();
26583
26811
  return vars[key] ?? "";
26584
26812
  });
26813
+ return out.replace(/\u0001O/g, "{").replace(/\u0001C/g, "}");
26585
26814
  }
26586
26815
  function isTruthy3(val) {
26587
26816
  if (!val) return false;
@@ -26785,6 +27014,15 @@ var init_streaming = __esm({
26785
27014
  });
26786
27015
 
26787
27016
  // ../../packages/compiler/tw/codegen/vdom-gen.ts
27017
+ function sanitizeUrlAttr3(name, value) {
27018
+ const URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "poster", "background"]);
27019
+ if (!URL_ATTRS.has(name.toLowerCase())) return value;
27020
+ const trimmed = String(value).trim().toLowerCase().replace(/[\s\0]/g, "");
27021
+ if (trimmed.startsWith("javascript:") || trimmed.startsWith("data:text/html") || trimmed.startsWith("vbscript:")) {
27022
+ return "#";
27023
+ }
27024
+ return value;
27025
+ }
26788
27026
  function generateVDOMCode(program, ctx) {
26789
27027
  const builder2 = new VDOMCodeBuilder(ctx);
26790
27028
  return builder2.generateModule(program);
@@ -26798,10 +27036,12 @@ function generateNodeVDOM(node, ctx) {
26798
27036
  return builder2.generateFromAST(node);
26799
27037
  }
26800
27038
  function interpolate4(expr, vars) {
26801
- return expr.replace(/\{([^}]+)\}/g, (_, name) => {
27039
+ const masked = expr.replace(/\\\{/g, "O").replace(/\\\}/g, "C");
27040
+ const out = masked.replace(/\{([^}]+)\}/g, (_, name) => {
26802
27041
  const key = name.trim();
26803
27042
  return vars[key] ?? "";
26804
27043
  });
27044
+ return out.replace(/\u0001O/g, "{").replace(/\u0001C/g, "}");
26805
27045
  }
26806
27046
  function isTruthy4(val) {
26807
27047
  if (!val) return false;
@@ -26935,7 +27175,7 @@ var init_vdom_gen = __esm({
26935
27175
  const expr = this.interpolateToExpr(attr.value);
26936
27176
  attrs.push(`${JSON.stringify(attr.name)}: ${expr}`);
26937
27177
  } else {
26938
- attrs.push(`${JSON.stringify(attr.name)}: ${JSON.stringify(attr.value)}`);
27178
+ attrs.push(`${JSON.stringify(attr.name)}: ${JSON.stringify(sanitizeUrlAttr3(attr.name, attr.value))}`);
26939
27179
  }
26940
27180
  }
26941
27181
  }
@@ -28952,7 +29192,7 @@ function generateVDOM(program, ctx) {
28952
29192
  return {
28953
29193
  t: el.tag,
28954
29194
  a: el.attrs.reduce((acc, a) => {
28955
- acc[a.name] = a.value;
29195
+ acc[a.name] = sanitizeUrlAttr4(a.name, String(a.value));
28956
29196
  return acc;
28957
29197
  }, {}),
28958
29198
  c: el.children.map(toVNode)
@@ -28990,16 +29230,27 @@ function processDirectives(program, ctx) {
28990
29230
  }
28991
29231
  }
28992
29232
  function interpolate5(expr, vars) {
28993
- return expr.replace(/\{([^}]+)\}/g, (_, name) => {
29233
+ const masked = expr.replace(/\\\{/g, "O").replace(/\\\}/g, "C");
29234
+ const out = masked.replace(/\{([^}]+)\}/g, (_, name) => {
28994
29235
  const key = name.trim();
28995
29236
  return vars[key] ?? "";
28996
29237
  });
29238
+ return out.replace(/\u0001O/g, "{").replace(/\u0001C/g, "}");
28997
29239
  }
28998
29240
  function isTruthy5(val) {
28999
29241
  if (!val) return false;
29000
29242
  const lower = val.toLowerCase().trim();
29001
29243
  return lower !== "false" && lower !== "0" && lower !== "" && lower !== "null" && lower !== "undefined";
29002
29244
  }
29245
+ function sanitizeUrlAttr4(name, value) {
29246
+ const URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "poster", "background"]);
29247
+ if (!URL_ATTRS.has(name.toLowerCase())) return value;
29248
+ const trimmed = String(value).trim().toLowerCase().replace(/[\s\0]/g, "");
29249
+ if (trimmed.startsWith("javascript:") || trimmed.startsWith("data:text/html") || trimmed.startsWith("vbscript:")) {
29250
+ return "#";
29251
+ }
29252
+ return value;
29253
+ }
29003
29254
  function generate(program, stateVars) {
29004
29255
  const startTime = performance.now();
29005
29256
  const ctx = createContext("ssr");
@@ -33080,7 +33331,12 @@ async function renderIslandContent(component, props, ctx) {
33080
33331
  return { html: result.html, css: result.css, js: result.js };
33081
33332
  }
33082
33333
  function countServerComponents(source) {
33083
- const matches = source.match(/@render\s+mode:\s*['"]server['"]/g);
33334
+ let cleaned = source;
33335
+ try {
33336
+ cleaned = stripCommentsStringAware(source);
33337
+ } catch {
33338
+ }
33339
+ const matches = cleaned.match(/@render\s+mode:\s*['"]server['"]/g);
33084
33340
  return matches ? matches.length : 0;
33085
33341
  }
33086
33342
  function determineHydrationStrategy(component, position) {
@@ -33118,6 +33374,7 @@ function parseClientDirective(source) {
33118
33374
  var componentRegistry2;
33119
33375
  var init_server_components = __esm({
33120
33376
  "../../packages/compiler/tw/server-components.ts"() {
33377
+ init_tw();
33121
33378
  init_tw4();
33122
33379
  componentRegistry2 = /* @__PURE__ */ new Map();
33123
33380
  }
@@ -40041,385 +40298,6 @@ var init_tw4 = __esm({
40041
40298
  }
40042
40299
  });
40043
40300
 
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
40301
  // ../../packages/server/tw/routing/twm-rules.ts
40424
40302
  var twm_rules_exports = {};
40425
40303
  __export(twm_rules_exports, {
@@ -40538,16 +40416,30 @@ function verifyJwt_hs256(token, secret) {
40538
40416
  return true;
40539
40417
  }
40540
40418
  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 + "/");
40419
+ const norm = (p) => {
40420
+ let x = p;
40421
+ try {
40422
+ x = decodeURIComponent(x);
40423
+ } catch {
40424
+ }
40425
+ if (x.length > 1 && x.endsWith("/")) x = x.slice(0, -1);
40426
+ return x;
40427
+ };
40428
+ const pat = norm(pattern);
40429
+ const path = norm(pathname);
40430
+ if (pat === "/**") return true;
40431
+ if (pat === "/*") return path === "" || path === "/";
40432
+ if (pat.endsWith("/**")) {
40433
+ const prefix = pat.slice(0, -3);
40434
+ return path === prefix || path.startsWith(prefix + "/");
40545
40435
  }
40546
- if (pattern.endsWith("/*")) {
40547
- const prefix = pattern.slice(0, -2);
40548
- return pathname === prefix || pathname.startsWith(prefix + "/");
40436
+ if (pat.endsWith("/*")) {
40437
+ const prefix = pat.slice(0, -2);
40438
+ if (!path.startsWith(prefix + "/")) return path === prefix;
40439
+ const rest = path.slice(prefix.length + 1);
40440
+ return !rest.includes("/");
40549
40441
  }
40550
- return pathname === pattern;
40442
+ return path === pat;
40551
40443
  }
40552
40444
  function ruleBlocked(rule, request, pathname) {
40553
40445
  const headers = request.headers;
@@ -40870,6 +40762,385 @@ var init_twm_rules = __esm({
40870
40762
  }
40871
40763
  });
40872
40764
 
40765
+ // ../../packages/shared/tw/node-import.ts
40766
+ var node_import_exports = {};
40767
+ __export(node_import_exports, {
40768
+ twImportTs: () => twImportTs
40769
+ });
40770
+ import { join as join10 } from "node:path";
40771
+ import { tmpdir } from "node:os";
40772
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
40773
+ async function twImportTs(tsPath) {
40774
+ if (!globalThis.__TW_NODE) {
40775
+ return import(tsPath);
40776
+ }
40777
+ const esbuildMain = globalThis.__TW_ESBUILD_MODULE;
40778
+ const esbuild = await (esbuildMain ? import(esbuildMain) : import("esbuild"));
40779
+ const safe = tsPath.replace(/[^a-zA-Z0-9]/g, "_").slice(-120);
40780
+ const cachePath = join10(tmpdir(), "tw-" + safe + ".mjs");
40781
+ await esbuild.build({
40782
+ entryPoints: [tsPath],
40783
+ bundle: true,
40784
+ format: "esm",
40785
+ platform: "node",
40786
+ target: "node18",
40787
+ outfile: cachePath,
40788
+ logLevel: "silent"
40789
+ });
40790
+ return import(pathToFileURL2(cachePath).href);
40791
+ }
40792
+ var init_node_import = __esm({
40793
+ "../../packages/shared/tw/node-import.ts"() {
40794
+ }
40795
+ });
40796
+
40797
+ // tw/commands/client-bundle.ts
40798
+ var client_bundle_exports = {};
40799
+ __export(client_bundle_exports, {
40800
+ BUILTIN_CLIENT_OK_SPECIFIERS: () => BUILTIN_CLIENT_OK_SPECIFIERS,
40801
+ RUNTIME_CLIENT_OK_SPECIFIERS: () => RUNTIME_CLIENT_OK_SPECIFIERS,
40802
+ buildClientChunk: () => buildClientChunk,
40803
+ buildPageScope: () => buildPageScope,
40804
+ collectClientImportsFromSource: () => collectClientImportsFromSource,
40805
+ findServerOnlyViolations: () => findServerOnlyViolations,
40806
+ importNames: () => importNames,
40807
+ isClientImportSpecifier: () => isClientImportSpecifier
40808
+ });
40809
+ import { join as join11, resolve as resolve3 } from "node:path";
40810
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
40811
+ import { spawnSync as spawnSync2 } from "node:child_process";
40812
+ function isClientImportSpecifier(spec) {
40813
+ if (spec.startsWith("@./") || spec.startsWith("./") || spec.startsWith("../")) return false;
40814
+ if (/\.(tw|tss|twm|ts|js)$/.test(spec)) return false;
40815
+ if (spec.startsWith("@tw/")) {
40816
+ return RUNTIME_CLIENT_OK_SPECIFIERS.includes(spec);
40817
+ }
40818
+ return true;
40819
+ }
40820
+ function collectClientImportsFromSource(src) {
40821
+ const lines = [];
40822
+ let m;
40823
+ IMPORT_RE.lastIndex = 0;
40824
+ while (m = IMPORT_RE.exec(src)) {
40825
+ if (isClientImportSpecifier(m[2])) lines.push(m[0].trim().replace(/;$/, ""));
40826
+ }
40827
+ return lines;
40828
+ }
40829
+ function importNames(line) {
40830
+ const clause = line.match(/^import\s+([\s\S]+?)\s+from/);
40831
+ if (!clause) return [];
40832
+ const c = clause[1].trim();
40833
+ const names = [];
40834
+ if (c.startsWith("*")) {
40835
+ const as = c.split(/\s+as\s+/);
40836
+ if (as[1]) names.push(as[1].trim());
40837
+ } else if (c.startsWith("{")) {
40838
+ const braces = c.match(/\{([\s\S]*)\}/);
40839
+ if (braces) {
40840
+ for (const part of braces[1].split(",")) {
40841
+ const p = part.trim();
40842
+ if (!p) continue;
40843
+ const seg = p.split(/\s+as\s+/);
40844
+ names.push(seg[seg.length - 1].trim());
40845
+ }
40846
+ }
40847
+ } else {
40848
+ const def = c.match(/^[A-Za-z_$][\w$]*/);
40849
+ if (def) names.push(def[0]);
40850
+ }
40851
+ return names;
40852
+ }
40853
+ function findEsbuildBin(rootDir) {
40854
+ const candidates = [
40855
+ join11(rootDir, "node_modules", ".bin", "esbuild"),
40856
+ resolve3(rootDir, "..", "node_modules", ".bin", "esbuild"),
40857
+ // CLI repo layout (dev / source runs)
40858
+ resolve3(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "node_modules", ".bin", "esbuild")
40859
+ ];
40860
+ for (const c of candidates) {
40861
+ try {
40862
+ if (existsSync7(c)) return c;
40863
+ } catch {
40864
+ }
40865
+ }
40866
+ const bd = globalThis.__TW_BUNDLE_DIR;
40867
+ if (bd) {
40868
+ const b = resolve3(bd, "..", "..", "..", "node_modules", ".bin", "esbuild");
40869
+ try {
40870
+ if (existsSync7(b)) return b;
40871
+ } catch {
40872
+ }
40873
+ }
40874
+ return "esbuild";
40875
+ }
40876
+ function findServerOnlyViolations(src) {
40877
+ const out = [];
40878
+ let m;
40879
+ IMPORT_RE.lastIndex = 0;
40880
+ while (m = IMPORT_RE.exec(src)) {
40881
+ const spec = m[2];
40882
+ if (spec === "lib" || spec.startsWith("lib/")) {
40883
+ out.push(spec);
40884
+ continue;
40885
+ }
40886
+ if (isClientImportSpecifier(spec)) continue;
40887
+ if (spec.startsWith("@tw/")) {
40888
+ if (BUILTIN_CLIENT_OK_SPECIFIERS.includes(spec)) continue;
40889
+ if (RUNTIME_CLIENT_OK_SPECIFIERS.includes(spec)) continue;
40890
+ out.push(spec);
40891
+ continue;
40892
+ }
40893
+ if (spec.startsWith("@./") || spec.startsWith("./") || spec.startsWith("../")) {
40894
+ if (/\.(tw|tss)$/.test(spec)) continue;
40895
+ out.push(spec);
40896
+ continue;
40897
+ }
40898
+ if (spec.startsWith("lib/")) out.push(spec);
40899
+ }
40900
+ return out;
40901
+ }
40902
+ function contentHash2(s) {
40903
+ let h = 5381;
40904
+ for (let i = 0; i < s.length; i++) h = (h * 33 ^ s.charCodeAt(i)) >>> 0;
40905
+ return h.toString(36);
40906
+ }
40907
+ function vendoredRuntimeClient() {
40908
+ const fromSrc = new URL("../../dist/tw-runtime-client.mjs", import.meta.url).pathname;
40909
+ if (existsSync7(fromSrc)) return fromSrc;
40910
+ const fromBundle = new URL("./tw-runtime-client.mjs", import.meta.url).pathname;
40911
+ if (existsSync7(fromBundle)) return fromBundle;
40912
+ return null;
40913
+ }
40914
+ function rewriteRuntimeSpecifier(importLine) {
40915
+ const vendored = vendoredRuntimeClient();
40916
+ if (!vendored) return importLine;
40917
+ const m = importLine.match(/from\s+["'](@tw\/runtime)["']/);
40918
+ if (!m) return importLine;
40919
+ return importLine.replace(m[0], `from ${JSON.stringify(vendored)}`);
40920
+ }
40921
+ function buildClientChunk(rootDir, importLine, minify) {
40922
+ if (!importLine) return "";
40923
+ const cached = chunkCache.get(importLine);
40924
+ if (cached && existsSync7(join11(rootDir, ".tw", cached))) return cached;
40925
+ const jsDir = join11(rootDir, ".tw", "js");
40926
+ mkdirSync4(jsDir, { recursive: true });
40927
+ const names = importNames(importLine);
40928
+ const entryLine = rewriteRuntimeSpecifier(importLine);
40929
+ const entry = [
40930
+ entryLine + ";",
40931
+ "globalThis.__twClient = globalThis.__twClient || {};",
40932
+ ...names.map((n) => "globalThis.__twClient." + n + " = " + n + ";")
40933
+ ].join("\n");
40934
+ const stamp = contentHash2(importLine);
40935
+ const entryPath = join11(rootDir, ".tw", ".chunk-entry-" + stamp + ".js");
40936
+ const tmpOut = join11(rootDir, ".tw", ".chunk-tmp-" + stamp + ".js");
40937
+ writeFileSync4(entryPath, entry);
40938
+ const bin = findEsbuildBin(rootDir);
40939
+ const args = [entryPath, "--bundle", "--format=iife", "--target=es2019", "--outfile=" + tmpOut];
40940
+ if (minify) args.push("--minify");
40941
+ const r = spawnSync2(bin, args, { cwd: rootDir });
40942
+ try {
40943
+ unlinkSync2(entryPath);
40944
+ } catch {
40945
+ }
40946
+ if (r.status !== 0) {
40947
+ const errText = (r.stderr && r.stderr.toString ? r.stderr.toString() : "") || String(r.error || "unknown");
40948
+ console.error(" [tw] client chunk failed (" + importLine + "): " + errText.split("\n")[0]);
40949
+ try {
40950
+ unlinkSync2(tmpOut);
40951
+ } catch {
40952
+ }
40953
+ return "";
40954
+ }
40955
+ const code = readFileSync6(tmpOut, "utf-8");
40956
+ const url = "js/c-" + contentHash2(code) + ".js";
40957
+ writeFileSync4(join11(rootDir, ".tw", url), code);
40958
+ try {
40959
+ unlinkSync2(tmpOut);
40960
+ } catch {
40961
+ }
40962
+ chunkCache.set(importLine, url);
40963
+ return url;
40964
+ }
40965
+ function buildPageScope(rootDir, ownImportLines) {
40966
+ if (!ownImportLines || ownImportLines.length === 0) return "";
40967
+ const names = ownImportLines.flatMap((l) => importNames(l));
40968
+ if (names.length === 0) return "";
40969
+ mkdirSync4(join11(rootDir, ".tw", "js"), { recursive: true });
40970
+ const body = names.map((n) => "p." + n + " = g.__twClient." + n + ";").join("\n");
40971
+ const code = "(function(g){g.__twClient=g.__twClient||{};var p={};\n" + body + "\ng.__twClient.$page=p;})(globalThis);\n";
40972
+ const url = "js/p-" + contentHash2(names.slice().sort().join("|")) + ".js";
40973
+ writeFileSync4(join11(rootDir, ".tw", url), code);
40974
+ return url;
40975
+ }
40976
+ var IMPORT_RE, BUILTIN_CLIENT_OK_SPECIFIERS, RUNTIME_CLIENT_OK_SPECIFIERS, chunkCache;
40977
+ var init_client_bundle = __esm({
40978
+ "tw/commands/client-bundle.ts"() {
40979
+ IMPORT_RE = /^[ \t]*import\s+([^\n]+?)\s+from\s+["'']([^"'']+)["''][; \t]*$/gm;
40980
+ BUILTIN_CLIENT_OK_SPECIFIERS = ["@tw/optImage", "@tw/RouterLink"];
40981
+ RUNTIME_CLIENT_OK_SPECIFIERS = ["@tw/runtime"];
40982
+ chunkCache = /* @__PURE__ */ new Map();
40983
+ }
40984
+ });
40985
+
40986
+ // ../../packages/server/tw/routing/revalidate.ts
40987
+ function setActivePipeline(p) {
40988
+ activePipeline = p;
40989
+ }
40990
+ function getActivePipeline() {
40991
+ return activePipeline;
40992
+ }
40993
+ function revalidatePath(pathname, opts) {
40994
+ return activePipeline ? activePipeline.revalidatePath(pathname, opts) : 0;
40995
+ }
40996
+ function registerTwmTagInvalidator(fn) {
40997
+ twmTagInvalidator = fn;
40998
+ }
40999
+ function revalidateTag(tag) {
41000
+ let dropped = 0;
41001
+ if (activePipeline) dropped += activePipeline.revalidateTag(tag);
41002
+ if (twmTagInvalidator) dropped += twmTagInvalidator(tag);
41003
+ return dropped;
41004
+ }
41005
+ var activePipeline, twmTagInvalidator;
41006
+ var init_revalidate = __esm({
41007
+ "../../packages/server/tw/routing/revalidate.ts"() {
41008
+ activePipeline = null;
41009
+ twmTagInvalidator = null;
41010
+ }
41011
+ });
41012
+
41013
+ // ../../packages/server/tw/routing/signal-stream.ts
41014
+ function getSignalHub() {
41015
+ if (!hub) hub = new SignalHub();
41016
+ return hub;
41017
+ }
41018
+ function setSignal(name, value, opts) {
41019
+ let session = opts?.session;
41020
+ if (!session && opts?.request) {
41021
+ try {
41022
+ const cookies = opts.request.cookies;
41023
+ if (cookies && typeof cookies.get === "function") {
41024
+ session = cookies.get("tw_session") ?? void 0;
41025
+ } else {
41026
+ const header = opts.request.headers?.get?.("cookie") ?? "";
41027
+ const m = /(?:^|;\s*)tw_session=([^;]+)/.exec(header);
41028
+ if (m) session = decodeURIComponent(m[1].trim().replace(/^"|"$/g, ""));
41029
+ }
41030
+ } catch {
41031
+ }
41032
+ }
41033
+ return getSignalHub().setSignal(name, value, session ? { session } : void 0);
41034
+ }
41035
+ var BATCH_MS, HISTORY_LIMIT, SignalHub, hub;
41036
+ var init_signal_stream = __esm({
41037
+ "../../packages/server/tw/routing/signal-stream.ts"() {
41038
+ BATCH_MS = 100;
41039
+ HISTORY_LIMIT = 1e3;
41040
+ SignalHub = class {
41041
+ clients = /* @__PURE__ */ new Map();
41042
+ nextClientId = 1;
41043
+ seq = 0;
41044
+ values = /* @__PURE__ */ new Map();
41045
+ kinds = /* @__PURE__ */ new Map();
41046
+ history = [];
41047
+ pending = /* @__PURE__ */ new Map();
41048
+ pendingSession;
41049
+ timer = null;
41050
+ /**
41051
+ * Server-side mutation (route handlers): queue an update, batch-flush.
41052
+ * With `session` (derived from the request's tw_session cookie) private
41053
+ * signals are delivered only to clients authenticated as that session.
41054
+ */
41055
+ setSignal(name, value, opts) {
41056
+ const kind = this.kinds.get(name) ?? "public";
41057
+ this.values.set(name, value);
41058
+ this.kinds.set(name, kind);
41059
+ this.pending.set(name, value);
41060
+ if (opts?.session) this.pendingSession = opts.session;
41061
+ if (this.timer == null) {
41062
+ this.timer = setTimeout(() => this.flush(), BATCH_MS);
41063
+ }
41064
+ return { queued: true, kind };
41065
+ }
41066
+ /** Latest known values (also used for fresh snapshots). */
41067
+ snapshot() {
41068
+ return { seq: this.seq, snapshot: Object.fromEntries(this.values) };
41069
+ }
41070
+ register(declared, session) {
41071
+ const id = this.nextClientId++;
41072
+ for (const [name, kind] of declared) {
41073
+ if (!this.kinds.has(name)) this.kinds.set(name, kind);
41074
+ }
41075
+ const client = {
41076
+ id,
41077
+ declared,
41078
+ session,
41079
+ send: () => {
41080
+ },
41081
+ close: () => this.clients.delete(id)
41082
+ };
41083
+ this.clients.set(id, client);
41084
+ return client;
41085
+ }
41086
+ /** Build the replay/snapshot preamble for a connecting client. */
41087
+ connectPayloads(since, declared, session) {
41088
+ const out = [];
41089
+ const hasCoverage = since <= this.seq && this.history.length > 0 && since >= this.history[0].seq - 1;
41090
+ if (hasCoverage) {
41091
+ for (const entry of this.history) {
41092
+ if (entry.seq > since && this.sessionMatches(session, entry) && this.deliversTo(declared, entry.names)) {
41093
+ out.push(entry.payload);
41094
+ }
41095
+ }
41096
+ } else if (this.values.size > 0) {
41097
+ const snap = {};
41098
+ for (const [name, value] of this.values) {
41099
+ if (this.sessionMatches(session, { names: [name], session: this.valueSessions.get(name) }) && this.deliversTo(declared, [name])) snap[name] = value;
41100
+ }
41101
+ if (Object.keys(snap).length > 0) {
41102
+ out.push(this.frame({ v: 1, seq: this.seq, snapshot: snap }));
41103
+ }
41104
+ }
41105
+ return out;
41106
+ }
41107
+ /** latest-known session per signal (for snapshot filtering) */
41108
+ valueSessions = /* @__PURE__ */ new Map();
41109
+ sessionMatches(clientSession, entry) {
41110
+ if (!entry.session) return true;
41111
+ return entry.session === clientSession;
41112
+ }
41113
+ deliversTo(declared, names) {
41114
+ return names.some((n) => {
41115
+ const kind = this.kinds.get(n) ?? "public";
41116
+ return kind === "public" || declared.has(n);
41117
+ });
41118
+ }
41119
+ flush() {
41120
+ this.timer = null;
41121
+ if (this.pending.size === 0) return;
41122
+ const updates = [...this.pending.entries()];
41123
+ const session = this.pendingSession;
41124
+ this.pending.clear();
41125
+ this.pendingSession = void 0;
41126
+ this.seq++;
41127
+ const names = updates.map(([n]) => n);
41128
+ for (const [n] of updates) this.valueSessions.set(n, session);
41129
+ const payload = this.frame({ v: 1, seq: this.seq, updates });
41130
+ this.history.push({ seq: this.seq, payload, names, session });
41131
+ if (this.history.length > HISTORY_LIMIT) this.history.shift();
41132
+ for (const client of this.clients.values()) {
41133
+ if (this.sessionMatches(client.session, { names, session }) && this.deliversTo(client.declared, names)) client.send(payload);
41134
+ }
41135
+ }
41136
+ frame(obj) {
41137
+ return JSON.stringify(obj).replace(/</g, "\\u003c");
41138
+ }
41139
+ };
41140
+ hub = null;
41141
+ }
41142
+ });
41143
+
40873
41144
  // ../../packages/server/tw/websocket-manager.ts
40874
41145
  var DEFAULT_MAX_ROOMS, DEFAULT_MAX_ROOM_SIZE, WebSocketManager;
40875
41146
  var init_websocket_manager = __esm({
@@ -41096,7 +41367,8 @@ function extractRenderMode(page) {
41096
41367
  try {
41097
41368
  const fs = __require("node:fs");
41098
41369
  const source = fs.readFileSync(page.absolutePath, "utf-8");
41099
- const m = source.match(/render\s+(static|ssr|island|edge)/);
41370
+ const { maskSourceStringsAndComments: maskSourceStringsAndComments2 } = (init_tw(), __toCommonJS(tw_exports));
41371
+ const m = maskSourceStringsAndComments2(source).match(/render\s+(static|ssr|island|edge)/);
41100
41372
  if (m) {
41101
41373
  return m[1];
41102
41374
  }
@@ -41744,7 +42016,7 @@ var init_render_pipeline = __esm({
41744
42016
  for (const headMatch of bodyHTML.matchAll(/<head[^>]*>([\s\S]*?)<\/head>/gi)) {
41745
42017
  const headInner = headMatch[1];
41746
42018
  const tMatch = headInner.match(/<title>([^<]*)<\/title>/i);
41747
- if (tMatch) layoutTitle = tMatch[1].trim();
42019
+ if (tMatch) layoutTitle = tMatch[1].trim().replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
41748
42020
  const kids = headInner.replace(/<title>[^<]*<\/title>/gi, "").replace(/<meta[^>]+charset[^>]*>/gi, "").replace(/<meta[^>]+viewport[^>]*>/gi, "").trim();
41749
42021
  if (kids) liftedParts.push(kids);
41750
42022
  }
@@ -41784,7 +42056,7 @@ ${bodyHTML}`;
41784
42056
  for (const hp of headCandidates) {
41785
42057
  const headCompiled = this.compileFile({ absolutePath: hp, type: "head" });
41786
42058
  const m = headCompiled.html.match(/<body>([\s\S]*)<\/body>/);
41787
- if (m && m[1].trim()) headExtra += m[1].trim() + "\n";
42059
+ if (m && m[1].trim()) headExtra += m[1].trim().replace(/\\([{}])/g, "$1") + "\n";
41788
42060
  }
41789
42061
  if (liftedHead) headExtra += liftedHead + "\n";
41790
42062
  } catch {
@@ -41982,8 +42254,13 @@ ${bodyHTML}`;
41982
42254
  extractTitle(file) {
41983
42255
  try {
41984
42256
  const source = readFileSync7(file.absolutePath, "utf-8");
41985
- const m = source.match(/page\s*\{[^}]*title\s+"([^"]+)"/);
41986
- return m ? m[1] : null;
42257
+ const masked = maskSourceStringsAndComments(source);
42258
+ const m = masked.match(/page\s*\{[^}]*title\s+"/);
42259
+ if (!m || m.index === void 0) return null;
42260
+ const start = m.index + m[0].length;
42261
+ const end = source.indexOf('"', start);
42262
+ if (end === -1 || end === start) return null;
42263
+ return source.slice(start, end).replace(/\\([{}"])/g, "$1");
41987
42264
  } catch {
41988
42265
  return null;
41989
42266
  }
@@ -41997,7 +42274,9 @@ ${bodyHTML}`;
41997
42274
  extractRenderMode(file) {
41998
42275
  try {
41999
42276
  const src = readFileSync7(file.absolutePath, "utf8");
42000
- const m = /render\s+(static|ssr|island|edge|csr|stream|ppr)\b/.exec(src);
42277
+ const m = /render\s+(static|ssr|island|edge|csr|stream|ppr)\b/.exec(
42278
+ maskSourceStringsAndComments(src)
42279
+ );
42001
42280
  return m ? m[1] : "static";
42002
42281
  } catch {
42003
42282
  return "static";
@@ -42045,7 +42324,7 @@ ${bodyHTML}`;
42045
42324
  extractRevalidate(file) {
42046
42325
  try {
42047
42326
  const source = readFileSync7(file.absolutePath, "utf-8");
42048
- const m = source.match(/page\s*\{[^}]*revalidate\s+(\d+)/);
42327
+ const m = maskSourceStringsAndComments(source).match(/page\s*\{[^}]*revalidate\s+(\d+)/);
42049
42328
  if (m) {
42050
42329
  const n = Number(m[1]);
42051
42330
  return Number.isFinite(n) && n > 0 ? n : null;
@@ -42091,14 +42370,32 @@ ${bodyHTML}`;
42091
42370
  buildMetaTags(file) {
42092
42371
  try {
42093
42372
  const source = readFileSync7(file.absolutePath, "utf-8");
42094
- const fm = source.match(/page\s*\{([^}]*)\}/);
42095
- if (!fm) return "";
42096
- const body = fm[1];
42373
+ const masked = maskSourceStringsAndComments(source);
42374
+ const fm = masked.match(/page\s*\{/);
42375
+ if (!fm || fm.index === void 0) return "";
42376
+ const bodyStart = fm.index + fm[0].length;
42377
+ const bodyEnd = masked.indexOf("}", bodyStart);
42378
+ if (bodyEnd === -1) return "";
42379
+ const body = source.slice(bodyStart, bodyEnd);
42097
42380
  const AMP = String.fromCharCode(38);
42098
42381
  const esc = (v) => v.split(AMP).join(AMP + "amp;").split(String.fromCharCode(34)).join(AMP + "quot;").split("<").join(AMP + "lt;");
42099
42382
  const pick = (key) => {
42100
- const m = body.match(new RegExp(key + '\\s+"([^"]*)"'));
42101
- return m ? m[1] : null;
42383
+ const i = body.indexOf(key + ' "');
42384
+ if (i === -1) return null;
42385
+ const rest = body.slice(i + key.length + 2);
42386
+ let end = -1;
42387
+ for (let j = 0; j < rest.length; j++) {
42388
+ if (rest[j] === "\\") {
42389
+ j++;
42390
+ continue;
42391
+ }
42392
+ if (rest[j] === '"') {
42393
+ end = j;
42394
+ break;
42395
+ }
42396
+ }
42397
+ if (end === -1) return null;
42398
+ return rest.slice(0, end).replace(/\\([{}"])/g, "$1");
42102
42399
  };
42103
42400
  let tags = "";
42104
42401
  const description = pick("description");
@@ -53507,6 +53804,13 @@ function generateSecurityHeaders(options = {}) {
53507
53804
  const headers = {};
53508
53805
  if (opts.csp) {
53509
53806
  headers["Content-Security-Policy"] = buildCSPHeader(opts.csp);
53807
+ try {
53808
+ const scriptSrc = Array.isArray(opts.csp["script-src"]) ? opts.csp["script-src"].join(" ") : String(opts.csp["script-src"] ?? "");
53809
+ if (scriptSrc && !scriptSrc.includes("'unsafe-eval'") && !scriptSrc.includes("'strict-dynamic'")) {
53810
+ 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).");
53811
+ }
53812
+ } catch {
53813
+ }
53510
53814
  }
53511
53815
  if (opts.hsts) {
53512
53816
  const hstsOpts = typeof opts.hsts === "object" ? opts.hsts : { maxAge: 31536e3, includeSubDomains: true };
@@ -54931,7 +55235,11 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
54931
55235
  }
54932
55236
  } else if (typeof request?.json === "function") {
54933
55237
  try {
54934
- parsedBody = await request.json();
55238
+ const rawBody2 = await request.text();
55239
+ if (rawBody2.length > MAX_BODY_BYTES) {
55240
+ return { status: 413, json: { ok: false, error: "Payload too large" } };
55241
+ }
55242
+ parsedBody = rawBody2 ? JSON.parse(rawBody2) : {};
54935
55243
  } catch {
54936
55244
  parsedBody = {};
54937
55245
  }
@@ -55211,7 +55519,9 @@ function findCompilerPath2() {
55211
55519
  return resolve6(process.cwd(), "packages/compiler/tw/index.ts");
55212
55520
  }
55213
55521
  function extractRenderMode2(src) {
55214
- const m = /render\s+(static|ssr|island|edge|csr|stream|ppr|signalStream)\b/.exec(src);
55522
+ const m = /render\s+(static|ssr|island|edge|csr|stream|ppr|signalStream)\b/.exec(
55523
+ maskSourceStringsAndComments(src)
55524
+ );
55215
55525
  return m ? m[1] : "static";
55216
55526
  }
55217
55527
  function csrifyHtml(html) {
@@ -55325,18 +55635,32 @@ function scanComponents2(rootDir) {
55325
55635
  for (const d of privDirs) registerComponentsFrom2(d);
55326
55636
  }
55327
55637
  function buildMetaTagsFromSource(source) {
55328
- const fm = source.match(/page\s*\{([^}]*)\}/);
55329
- if (!fm) return "";
55330
- const body = fm[1];
55638
+ const masked = maskSourceStringsAndComments(source);
55639
+ const fm = masked.match(/page\s*\{/);
55640
+ if (!fm || fm.index === void 0) return "";
55641
+ const bodyStart = fm.index + fm[0].length;
55642
+ const bodyEnd = masked.indexOf("}", bodyStart);
55643
+ if (bodyEnd === -1) return "";
55644
+ const body = source.slice(bodyStart, bodyEnd);
55331
55645
  const AMP = String.fromCharCode(38);
55332
55646
  const esc = (v) => v.split(AMP).join(AMP + "amp;").split(String.fromCharCode(34)).join(AMP + "quot;").split("<").join(AMP + "lt;");
55333
55647
  const pick = (key) => {
55334
55648
  const i = body.indexOf(key + ' "');
55335
55649
  if (i === -1) return null;
55336
55650
  const rest = body.slice(i + key.length + 2);
55337
- const end = rest.indexOf('"');
55651
+ let end = -1;
55652
+ for (let j = 0; j < rest.length; j++) {
55653
+ if (rest[j] === "\\") {
55654
+ j++;
55655
+ continue;
55656
+ }
55657
+ if (rest[j] === '"') {
55658
+ end = j;
55659
+ break;
55660
+ }
55661
+ }
55338
55662
  if (end === -1) return null;
55339
- return rest.slice(0, end);
55663
+ return rest.slice(0, end).replace(/\\([{}"])/g, "$1");
55340
55664
  };
55341
55665
  let tags = "";
55342
55666
  const description = pick("description");
@@ -55466,7 +55790,7 @@ async function buildCommand() {
55466
55790
  errorCount++;
55467
55791
  }
55468
55792
  }
55469
- const revM = resolved ? null : /page\s*\{[^}]*revalidate\s+(\d+)/.exec(pageSrc);
55793
+ const revM = resolved ? null : /page\s*\{[^}]*revalidate\s+(\d+)/.exec(maskSourceStringsAndComments(pageSrc));
55470
55794
  if (resolved || revM) {
55471
55795
  for (const ps of paramSets) {
55472
55796
  const rp = routePath.split("/").map((seg) => {
@@ -55550,7 +55874,10 @@ async function buildCommand() {
55550
55874
  const headResult = _compileSync(headSource, { filePath: headSrc });
55551
55875
  const headBody = headResult.html.match(/<body>([\s\S]*)<\/body>/);
55552
55876
  if (headBody && headBody[1].trim()) {
55553
- html = html.replace("</head>", " " + headBody[1].trim() + "\n</head>");
55877
+ html = html.replace(
55878
+ "</head>",
55879
+ " " + headBody[1].trim().replace(/\\([{}])/g, "$1") + "\n</head>"
55880
+ );
55554
55881
  }
55555
55882
  } catch (e) {
55556
55883
  console.log(" Warning: head.tw failed: " + e.message);
@@ -55761,6 +56088,7 @@ async function buildCommand() {
55761
56088
  }
55762
56089
  console.log("\n \x1B[32mBuild complete!\x1B[0m");
55763
56090
  console.log(" Pages: " + pageCount + " compiled");
56091
+ 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
56092
  console.log(" APIs: " + apiCount + " routes");
55765
56093
  if (isrRoutes.size > 0) {
55766
56094
  const manifest = {};
@@ -57709,9 +58037,11 @@ var init_provider2 = __esm({
57709
58037
  getExpressionCompletions(doc) {
57710
58038
  const items = [];
57711
58039
  const definedVars = /* @__PURE__ */ new Set();
57712
- const scriptMatch = doc.content.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
58040
+ const scriptMatches = [...doc.content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi)];
58041
+ const scriptMatch = scriptMatches.length ? [scriptMatches.map((m) => m[1]).join("\n")] : null;
58042
+ const scriptContent = scriptMatch ? scriptMatch[0] : "";
57713
58043
  if (scriptMatch) {
57714
- const sc = scriptMatch[1];
58044
+ const sc = scriptContent;
57715
58045
  const varRegex = /(?:const|let|var)\s+(\w+)/g;
57716
58046
  let m;
57717
58047
  while ((m = varRegex.exec(sc)) !== null) {
@@ -58286,8 +58616,10 @@ var init_symbols = __esm({
58286
58616
  const doc = this.docManager.getDocument(uri);
58287
58617
  if (!doc) return [];
58288
58618
  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)));
58619
+ const scriptMatches = [...doc.content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi)];
58620
+ const scriptMatch = scriptMatches.length ? [scriptMatches.map((m) => m[1]).join("\n")] : null;
58621
+ const scriptContent = scriptMatch ? scriptMatch[0] : "";
58622
+ if (scriptMatch) symbols.push(...this.parseScriptSymbols(scriptContent, this.findScriptStart(doc)));
58291
58623
  symbols.push(...this.parseTemplateSymbols(doc));
58292
58624
  return symbols;
58293
58625
  }
@@ -58600,6 +58932,11 @@ var init_provider14 = __esm({
58600
58932
  });
58601
58933
 
58602
58934
  // ../../packages/lsp/tw/code-lens/provider.ts
58935
+ function stripLineComments(src) {
58936
+ return src.split("\n").map(function(l) {
58937
+ return l.replace(/(^|\s)\/\/.*$/, "");
58938
+ }).join("\n");
58939
+ }
58603
58940
  var CodeLensProvider;
58604
58941
  var init_provider15 = __esm({
58605
58942
  "../../packages/lsp/tw/code-lens/provider.ts"() {
@@ -58632,7 +58969,7 @@ var init_provider15 = __esm({
58632
58969
  const esc = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58633
58970
  const regex = new RegExp(`\\b${esc}\\b`, "g");
58634
58971
  for (const d of this.docManager.getAllDocuments()) {
58635
- const m = d.content.match(regex);
58972
+ const m = stripLineComments(d.content).match(regex);
58636
58973
  if (m) count += m.length - 1;
58637
58974
  }
58638
58975
  return Math.max(0, count);
@@ -58642,7 +58979,7 @@ var init_provider15 = __esm({
58642
58979
  const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58643
58980
  const regex = new RegExp(`<${esc}\\b`, "g");
58644
58981
  for (const d of this.docManager.getAllDocuments()) {
58645
- const m = d.content.match(regex);
58982
+ const m = stripLineComments(d.content).match(regex);
58646
58983
  if (m) count += m.length;
58647
58984
  }
58648
58985
  return count;
@@ -60983,38 +61320,15 @@ function destroySession(req) {
60983
61320
  const match = cookie.match(new RegExp(`${SESSION_COOKIE}=([^;]+)`));
60984
61321
  if (match) sessions.delete(match[1]);
60985
61322
  }
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 + "/");
61323
+ async function loadMiddlewareRules(mwPath) {
61324
+ try {
61325
+ const source = readFileSync11(mwPath, "utf-8");
61326
+ const { parseRules: parseRules2 } = await Promise.resolve().then(() => (init_twm_rules(), twm_rules_exports));
61327
+ return parseRules2(source);
61328
+ } catch (e) {
61329
+ console.log(" Middleware error: " + e.message);
61330
+ return [];
61016
61331
  }
61017
- return false;
61018
61332
  }
61019
61333
  function get404() {
61020
61334
  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 +61407,8 @@ async function devCommand() {
61093
61407
  if (existsSync15(stylePath)) parts.push(readFileSync11(stylePath, "utf-8"));
61094
61408
  if (parts.length) css = parts.join("\n");
61095
61409
  }
61096
- const middlewareRules = loadMiddleware(rootDir);
61410
+ const mwPath = join19(rootDir, "middleware.twm");
61411
+ const middlewareRules = existsSync15(mwPath) ? await loadMiddlewareRules(mwPath) : [];
61097
61412
  scanComponents(rootDir);
61098
61413
  const compDir = join19(rootDir, "components");
61099
61414
  if (existsSync15(compDir)) {
@@ -61168,13 +61483,13 @@ async function devCommand() {
61168
61483
  }
61169
61484
  async function runUserMiddleware(req) {
61170
61485
  try {
61171
- const mwPath = join19(rootDir, "middleware.twm");
61172
- if (!existsSync15(mwPath)) return null;
61486
+ const mwPath2 = join19(rootDir, "middleware.twm");
61487
+ if (!existsSync15(mwPath2)) return null;
61173
61488
  const { executeMiddleware: executeMiddleware2, loadTWMModule: loadTWMModule2, shouldMatchMiddleware: shouldMatchMiddleware2 } = await Promise.resolve().then(() => (init_twm_loader(), twm_loader_exports));
61174
- const mod = await loadTWMModule2(mwPath, rootDir);
61489
+ const mod = await loadTWMModule2(mwPath2, rootDir);
61175
61490
  const mwPathname = new URL(req.url).pathname;
61176
61491
  if (!mod.config?.matcher || shouldMatchMiddleware2(mod.config.matcher, mwPathname)) {
61177
- const mwResult = await executeMiddleware2(mwPath, req, rootDir);
61492
+ const mwResult = await executeMiddleware2(mwPath2, req, rootDir);
61178
61493
  if (mwResult instanceof Response) return mwResult;
61179
61494
  if (mwResult && typeof mwResult === "object" && mwResult.headers && !mwResult.status && !mwResult.json) {
61180
61495
  return mwResult.headers;
@@ -61230,16 +61545,15 @@ async function devCommand() {
61230
61545
  rescanComponentsIfNeeded(rootDir);
61231
61546
  console.log(" [" + time + "] " + req.method + " " + path);
61232
61547
  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
- }
61548
+ try {
61549
+ const { evaluateRules: evaluateRules2 } = await Promise.resolve().then(() => (init_twm_rules(), twm_rules_exports));
61550
+ const mwRes = evaluateRules2(middlewareRules, req);
61551
+ if (mwRes) {
61552
+ console.log(" -> middleware response " + mwRes.status + " for " + path);
61553
+ return mwRes;
61242
61554
  }
61555
+ } catch (e) {
61556
+ console.log(" Middleware error: " + e.message);
61243
61557
  }
61244
61558
  if (path === "/__tw_runtime.js") {
61245
61559
  const _fu = import.meta.url.replace("file://", "");
@@ -61449,13 +61763,20 @@ async function devCommand() {
61449
61763
  if (/<\/body>/.test(html)) {
61450
61764
  html = html.replace("</body>", devClientTag + ' <script src="/__tw_runtime.js"></script>\n</body>');
61451
61765
  }
61766
+ const devGlobalHead = join19(homeDir, "head.tw");
61767
+ const devHeadSources = [];
61768
+ if (existsSync15(devGlobalHead)) devHeadSources.push(devGlobalHead);
61452
61769
  const headTw = join19(dirname5(resolved.file), "head.tw");
61453
- if (existsSync15(headTw)) {
61770
+ if (headTw !== devGlobalHead && existsSync15(headTw)) devHeadSources.push(headTw);
61771
+ for (const devHeadTw of devHeadSources) {
61454
61772
  try {
61455
- const headRes = compileSync3(readFileSync11(headTw, "utf-8"), { filePath: headTw });
61773
+ const headRes = compileSync3(readFileSync11(devHeadTw, "utf-8"), { filePath: devHeadTw });
61456
61774
  const headBody = headRes.html.match(/<body>([\s\S]*)<\/body>/);
61457
61775
  if (headBody && headBody[1].trim()) {
61458
- html = html.replace("</head>", " " + headBody[1].trim() + "\n</head>");
61776
+ html = html.replace(
61777
+ "</head>",
61778
+ " " + headBody[1].trim().replace(/\\([{}])/g, "$1") + "\n</head>"
61779
+ );
61459
61780
  }
61460
61781
  } catch (e) {
61461
61782
  console.log(" Warning: head.tw failed: " + e.message);
@@ -61987,6 +62308,7 @@ async function testCommand() {
61987
62308
  }
61988
62309
 
61989
62310
  // tw/commands/check.ts
62311
+ init_tw();
61990
62312
  import { join as join28 } from "node:path";
61991
62313
  import { existsSync as existsSync23, readFileSync as readFileSync15, readdirSync as readdirSync10, statSync as statSync10 } from "node:fs";
61992
62314
  async function checkCommand() {
@@ -62044,23 +62366,24 @@ async function checkCommand() {
62044
62366
  for (const filePath of twFiles) {
62045
62367
  const relativePath = filePath.replace(rootDir + "/", "");
62046
62368
  const source = readFileSync15(filePath, "utf-8");
62047
- const openBraces = (source.match(/\{/g) || []).length;
62048
- const closeBraces = (source.match(/\}/g) || []).length;
62369
+ const maskedSource = maskSourceStringsAndComments(source);
62370
+ const openBraces = (maskedSource.match(/\{/g) || []).length;
62371
+ const closeBraces = (maskedSource.match(/\}/g) || []).length;
62049
62372
  if (openBraces !== closeBraces) {
62050
62373
  totalErrors++;
62051
- console.debug(` X ${relativePath}: unbalanced braces (${openBraces} open, ${closeBraces} close)`);
62374
+ console.log(` X ${relativePath}: unbalanced braces (${openBraces} open, ${closeBraces} close)`);
62052
62375
  } else {
62053
- console.debug(` OK ${relativePath} -- syntax OK`);
62376
+ console.log(` OK ${relativePath} -- syntax OK`);
62054
62377
  }
62055
62378
  }
62056
62379
  }
62057
- console.debug(`
62380
+ console.log(`
62058
62381
  Summary: ${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info`);
62059
62382
  if (totalErrors > 0) {
62060
- console.debug(" Status: FAILED\n");
62383
+ console.log(" Status: FAILED\n");
62061
62384
  process.exit(1);
62062
62385
  } else {
62063
- console.debug(" Status: PASSED\n");
62386
+ console.log(" Status: PASSED\n");
62064
62387
  }
62065
62388
  }
62066
62389
  function collectFiles2(dir, ext, results) {
@@ -62077,11 +62400,12 @@ function collectFiles2(dir, ext, results) {
62077
62400
  }
62078
62401
 
62079
62402
  // tw/commands/plugin.ts
62403
+ init_tw();
62080
62404
  import { join as join29 } from "node:path";
62081
62405
  import { existsSync as existsSync24, readFileSync as readFileSync16, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, readdirSync as readdirSync11 } from "node:fs";
62082
62406
  function readConfigPlugins(cfgPath) {
62083
62407
  if (!existsSync24(cfgPath)) return [];
62084
- const src = readFileSync16(cfgPath, "utf-8");
62408
+ const src = stripCommentsStringAware(readFileSync16(cfgPath, "utf-8"));
62085
62409
  const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
62086
62410
  if (!m) return [];
62087
62411
  const out = [];
@@ -62100,7 +62424,7 @@ function addConfigPlugin(cfgPath, name) {
62100
62424
  `);
62101
62425
  return true;
62102
62426
  }
62103
- const src = readFileSync16(cfgPath, "utf-8");
62427
+ const src = stripCommentsStringAware(readFileSync16(cfgPath, "utf-8"));
62104
62428
  if (new RegExp(`["'\`]${name}["'\`]`).test(src)) return false;
62105
62429
  const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
62106
62430
  if (m) {
@@ -62117,7 +62441,7 @@ function addConfigPlugin(cfgPath, name) {
62117
62441
  }
62118
62442
  function removeConfigPlugin(cfgPath, name) {
62119
62443
  if (!existsSync24(cfgPath)) return false;
62120
- const src = readFileSync16(cfgPath, "utf-8");
62444
+ const src = stripCommentsStringAware(readFileSync16(cfgPath, "utf-8"));
62121
62445
  const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
62122
62446
  if (!m) return false;
62123
62447
  const kept = m[1].split(",").map((e) => e.trim()).filter((e) => e && e !== `"${name}"` && e !== `'${name}'` && e !== "`" + name + "`");