tw-framework 1.0.5 → 1.0.6

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.
Files changed (3) hide show
  1. package/dist/testing.cjs +1640 -1063
  2. package/dist/tw.mjs +7823 -840
  3. package/package.json +1 -1
package/dist/testing.cjs CHANGED
@@ -39,10 +39,20 @@ function getActivePipeline() {
39
39
  function revalidatePath(pathname, opts) {
40
40
  return activePipeline ? activePipeline.revalidatePath(pathname, opts) : 0;
41
41
  }
42
- var activePipeline;
42
+ function registerTwmTagInvalidator(fn) {
43
+ twmTagInvalidator = fn;
44
+ }
45
+ function revalidateTag(tag) {
46
+ let dropped = 0;
47
+ if (activePipeline) dropped += activePipeline.revalidateTag(tag);
48
+ if (twmTagInvalidator) dropped += twmTagInvalidator(tag);
49
+ return dropped;
50
+ }
51
+ var activePipeline, twmTagInvalidator;
43
52
  var init_revalidate = __esm({
44
53
  "../../packages/server/tw/routing/revalidate.ts"() {
45
54
  activePipeline = null;
55
+ twmTagInvalidator = null;
46
56
  }
47
57
  });
48
58
 
@@ -627,905 +637,486 @@ var init_twm_rules = __esm({
627
637
  }
628
638
  });
629
639
 
630
- // ../../packages/shared/tw/node-import.ts
631
- var node_import_exports = {};
632
- __export(node_import_exports, {
633
- twImportTs: () => twImportTs
634
- });
635
- async function twImportTs(tsPath) {
636
- if (!globalThis.__TW_NODE) {
637
- return import(tsPath);
640
+ // ../../packages/shared/tw/hashing/md5.ts
641
+ var init_md5 = __esm({
642
+ "../../packages/shared/tw/hashing/md5.ts"() {
638
643
  }
639
- const esbuildMain = globalThis.__TW_ESBUILD_MODULE;
640
- const esbuild = await (esbuildMain ? import(esbuildMain) : import("esbuild"));
641
- const safe = tsPath.replace(/[^a-zA-Z0-9]/g, "_").slice(-120);
642
- const cachePath = (0, import_node_path.join)((0, import_node_os.tmpdir)(), "tw-" + safe + ".mjs");
643
- await esbuild.build({
644
- entryPoints: [tsPath],
645
- bundle: true,
646
- format: "esm",
647
- platform: "node",
648
- target: "node18",
649
- outfile: cachePath,
650
- logLevel: "silent"
651
- });
652
- return import((0, import_node_url.pathToFileURL)(cachePath).href);
653
- }
654
- var import_node_path, import_node_os, import_node_url;
655
- var init_node_import = __esm({
656
- "../../packages/shared/tw/node-import.ts"() {
657
- import_node_path = require("node:path");
658
- import_node_os = require("node:os");
659
- import_node_url = require("node:url");
644
+ });
645
+
646
+ // ../../packages/shared/tw/hashing/crc32.ts
647
+ var crc32Table;
648
+ var init_crc32 = __esm({
649
+ "../../packages/shared/tw/hashing/crc32.ts"() {
650
+ crc32Table = (() => {
651
+ const table = new Uint32Array(256);
652
+ for (let i = 0; i < 256; i++) {
653
+ let crc = i;
654
+ for (let j = 0; j < 8; j++) {
655
+ crc = crc & 1 ? 3988292384 ^ crc >>> 1 : crc >>> 1;
656
+ }
657
+ table[i] = crc >>> 0;
658
+ }
659
+ return table;
660
+ })();
660
661
  }
661
662
  });
662
663
 
663
- // ../../packages/image/tw/index.ts
664
- function optimizedImageUrl(src, opts = {}) {
665
- const p = new URLSearchParams();
666
- if (opts.width) p.set("w", String(opts.width));
667
- if (opts.height) p.set("h", String(opts.height));
668
- if (opts.quality) p.set("q", String(opts.quality));
669
- p.set("f", opts.format ?? "auto");
670
- const qs = p.toString();
671
- return `/_tw/img${src.startsWith("/") ? src : "/" + src}?${qs}`;
664
+ // ../../packages/shared/tw/hashing/sha.ts
665
+ function syncHashFallback(algorithm, data) {
666
+ const input = typeof data === "string" ? data : new TextDecoder().decode(data);
667
+ let hash2 = 2166136261;
668
+ for (let i = 0; i < input.length; i++) {
669
+ hash2 ^= input.charCodeAt(i);
670
+ hash2 = Math.imul(hash2, 16777619);
671
+ }
672
+ return (hash2 >>> 0).toString(16).padStart(8, "0").repeat(8);
672
673
  }
673
- function imageAttributesFor(opts, config = DEFAULT_IMAGE_CONFIG) {
674
- const width = opts.width ?? 0;
675
- const quality = opts.quality ?? config.quality;
676
- const format = config.formats[0] ?? "webp";
677
- const isRemote = /^https?:\/\//i.test(opts.src);
678
- const src = isRemote ? `/_tw/img/_remote?src=${encodeURIComponent(opts.src)}&w=${opts.width ?? ""}&h=${opts.height ?? ""}&q=${quality}&f=auto` : optimizedImageUrl(opts.src, { width: opts.width, height: opts.height, quality });
679
- const widths = config.breakpoints.filter((bp) => width === 0 || bp <= width * 2);
680
- const srcset = widths.map(
681
- (bp) => isRemote ? `/_tw/img/_remote?src=${encodeURIComponent(opts.src)}&w=${bp}&q=${quality}&f=auto ${bp}w` : `${optimizedImageUrl(opts.src, { width: bp, quality })} ${bp}w`
682
- ).join(", ");
683
- const sizes = width > 0 ? `(max-width: ${width}px) 100vw, ${width}px` : "100vw";
684
- const loading = opts.priority ? "eager" : "lazy";
685
- return { src, srcset, sizes, width, height: opts.height ?? 0, loading, decoding: "async", format };
674
+ function sha256(data) {
675
+ try {
676
+ const hasher = new BunHashSync("sha256");
677
+ return hasher.update(data).digest("hex");
678
+ } catch {
679
+ return syncHashFallback("sha256", data);
680
+ }
686
681
  }
687
- function createImageHandler(options) {
688
- const rootDir = (0, import_node_path2.resolve)(options.rootDir);
689
- const sourceDir = (0, import_node_path2.resolve)(options.sourceDir ?? (0, import_node_path2.join)(rootDir, "public"));
690
- const cacheDir = (0, import_node_path2.resolve)(options.cacheDir ?? (0, import_node_path2.join)(rootDir, ".tw", "img", "cache"));
691
- const config = { ...DEFAULT_IMAGE_CONFIG, ...options.config };
692
- let sharpPromise = null;
693
- let warnedNoSharp = false;
694
- async function loadSharp() {
695
- if (!sharpPromise) {
696
- sharpPromise = (async () => {
697
- let req;
698
- try {
699
- const { createRequire } = await import("node:module");
700
- req = createRequire((0, import_node_path2.join)(rootDir, "package.json"));
701
- } catch {
702
- }
703
- const candidates = [
704
- (0, import_node_path2.join)(rootDir, "node_modules", "sharp"),
705
- "sharp"
706
- ];
707
- for (const candidate of candidates) {
708
- if (typeof req === "function") {
709
- try {
710
- const m = req(candidate);
711
- const mod = m?.default ?? m;
712
- if (mod && typeof mod === "function") return mod;
713
- } catch {
714
- }
715
- }
716
- try {
717
- const href = candidate.startsWith("/") ? (0, import_node_url2.pathToFileURL)(candidate).href : candidate;
718
- const m = await import(href);
719
- const mod = m?.default ?? m;
720
- if (mod && typeof mod === "function") return mod;
721
- } catch {
722
- }
682
+ var BunHashSync;
683
+ var init_sha = __esm({
684
+ "../../packages/shared/tw/hashing/sha.ts"() {
685
+ init_md5();
686
+ init_crc32();
687
+ BunHashSync = class {
688
+ hasher;
689
+ constructor(algorithm) {
690
+ const nodeCrypto = globalThis.require?.("node:crypto") || globalThis.crypto;
691
+ if (nodeCrypto?.createHash) {
692
+ this.hasher = nodeCrypto.createHash(algorithm);
693
+ } else {
694
+ throw new Error("No sync hash available");
723
695
  }
724
- return null;
725
- })();
726
- }
727
- return sharpPromise;
696
+ }
697
+ update(data) {
698
+ const input = typeof data === "string" ? data : Buffer.from(data);
699
+ this.hasher.update(input);
700
+ return this;
701
+ }
702
+ digest(encoding) {
703
+ return this.hasher.digest(encoding);
704
+ }
705
+ };
728
706
  }
729
- function contentTypeFor(path) {
730
- return IMAGE_EXTENSIONS[(0, import_node_path2.extname)(path).toLowerCase()] ?? "application/octet-stream";
707
+ });
708
+
709
+ // ../../packages/shared/tw/utils.ts
710
+ var init_utils = __esm({
711
+ "../../packages/shared/tw/utils.ts"() {
731
712
  }
732
- function negotiateFormat(requested, accept) {
733
- if (requested && requested !== "auto") {
734
- return SHARP_FORMATS[requested.toLowerCase()] ?? null;
713
+ });
714
+
715
+ // ../../packages/shared/tw/fs/runtime-compat.ts
716
+ var init_runtime_compat = __esm({
717
+ "../../packages/shared/tw/fs/runtime-compat.ts"() {
718
+ }
719
+ });
720
+
721
+ // ../../packages/shared/tw/constants/misc.ts
722
+ var MIME_TYPES, VLQ_BASE_SHIFT, VLQ_BASE, VLQ_BASE_MASK;
723
+ var init_misc = __esm({
724
+ "../../packages/shared/tw/constants/misc.ts"() {
725
+ MIME_TYPES = {
726
+ ".html": "text/html",
727
+ ".htm": "text/html",
728
+ ".css": "text/css",
729
+ ".js": "text/javascript",
730
+ ".mjs": "text/javascript",
731
+ ".json": "application/json",
732
+ ".xml": "application/xml",
733
+ ".txt": "text/plain",
734
+ ".md": "text/markdown",
735
+ ".csv": "text/csv",
736
+ ".svg": "image/svg+xml",
737
+ ".png": "image/png",
738
+ ".jpg": "image/jpeg",
739
+ ".jpeg": "image/jpeg",
740
+ ".gif": "image/gif",
741
+ ".webp": "image/webp",
742
+ ".avif": "image/avif",
743
+ ".ico": "image/x-icon",
744
+ ".bmp": "image/bmp",
745
+ ".woff": "font/woff",
746
+ ".woff2": "font/woff2",
747
+ ".ttf": "font/ttf",
748
+ ".otf": "font/otf",
749
+ ".pdf": "application/pdf",
750
+ ".zip": "application/zip",
751
+ ".gz": "application/gzip",
752
+ ".tar": "application/x-tar",
753
+ ".mp4": "video/mp4",
754
+ ".webm": "video/webm",
755
+ ".mp3": "audio/mpeg",
756
+ ".ogg": "audio/ogg",
757
+ ".wav": "audio/wav",
758
+ ".wasm": "application/wasm",
759
+ ".tw": "text/tw",
760
+ ".tss": "text/tss",
761
+ ".toml": "application/toml",
762
+ ".yaml": "application/yaml",
763
+ ".yml": "application/yaml",
764
+ ".webmanifest": "application/manifest+json"
765
+ };
766
+ VLQ_BASE_SHIFT = 5;
767
+ VLQ_BASE = 1 << VLQ_BASE_SHIFT;
768
+ VLQ_BASE_MASK = VLQ_BASE - 1;
769
+ }
770
+ });
771
+
772
+ // ../../packages/shared/tw/fs/async.ts
773
+ var init_async = __esm({
774
+ "../../packages/shared/tw/fs/async.ts"() {
775
+ init_runtime_compat();
776
+ init_runtime_compat();
777
+ init_misc();
778
+ }
779
+ });
780
+
781
+ // ../../packages/runtime/tw/utils/path-utils.ts
782
+ var init_path_utils = __esm({
783
+ "../../packages/runtime/tw/utils/path-utils.ts"() {
784
+ }
785
+ });
786
+
787
+ // ../../packages/shared/tw/fs/glob.ts
788
+ var init_glob = __esm({
789
+ "../../packages/shared/tw/fs/glob.ts"() {
790
+ init_runtime_compat();
791
+ init_path_utils();
792
+ }
793
+ });
794
+
795
+ // ../../packages/shared/tw/fs/sync.ts
796
+ var init_sync = __esm({
797
+ "../../packages/shared/tw/fs/sync.ts"() {
798
+ init_runtime_compat();
799
+ init_runtime_compat();
800
+ init_async();
801
+ }
802
+ });
803
+
804
+ // ../../packages/shared/tw/fs/watch.ts
805
+ var init_watch = __esm({
806
+ "../../packages/shared/tw/fs/watch.ts"() {
807
+ init_sync();
808
+ init_runtime_compat();
809
+ }
810
+ });
811
+
812
+ // ../../packages/shared/tw/fs/index.ts
813
+ var init_fs = __esm({
814
+ "../../packages/shared/tw/fs/index.ts"() {
815
+ init_async();
816
+ init_glob();
817
+ init_sync();
818
+ init_watch();
819
+ }
820
+ });
821
+
822
+ // ../../packages/shared/tw/config/defaults.ts
823
+ var init_defaults = __esm({
824
+ "../../packages/shared/tw/config/defaults.ts"() {
825
+ init_fs();
826
+ }
827
+ });
828
+
829
+ // ../../packages/shared/tw/logger/levels.ts
830
+ var init_levels = __esm({
831
+ "../../packages/shared/tw/logger/levels.ts"() {
832
+ }
833
+ });
834
+
835
+ // ../../packages/shared/tw/logger/colors.ts
836
+ var ANSI, LEVEL_COLORS, LEVEL_ICONS;
837
+ var init_colors = __esm({
838
+ "../../packages/shared/tw/logger/colors.ts"() {
839
+ init_levels();
840
+ ANSI = {
841
+ reset: "\x1B[0m",
842
+ bold: "\x1B[1m",
843
+ dim: "\x1B[2m",
844
+ italic: "\x1B[3m",
845
+ underline: "\x1B[4m",
846
+ blink: "\x1B[5m",
847
+ inverse: "\x1B[7m",
848
+ hidden: "\x1B[8m",
849
+ strikethrough: "\x1B[9m",
850
+ black: "\x1B[30m",
851
+ red: "\x1B[31m",
852
+ green: "\x1B[32m",
853
+ yellow: "\x1B[33m",
854
+ blue: "\x1B[34m",
855
+ magenta: "\x1B[35m",
856
+ cyan: "\x1B[36m",
857
+ white: "\x1B[37m",
858
+ bgBlack: "\x1B[40m",
859
+ bgRed: "\x1B[41m",
860
+ bgGreen: "\x1B[42m",
861
+ bgYellow: "\x1B[44m",
862
+ bgBlue: "\x1B[44m",
863
+ bgMagenta: "\x1B[45m",
864
+ bgCyan: "\x1B[46m",
865
+ bgWhite: "\x1B[47m",
866
+ brightBlack: "\x1B[90m",
867
+ brightRed: "\x1B[91m",
868
+ brightGreen: "\x1B[92m",
869
+ brightYellow: "\x1B[93m",
870
+ brightBlue: "\x1B[94m",
871
+ brightMagenta: "\x1B[95m",
872
+ brightCyan: "\x1B[96m",
873
+ brightWhite: "\x1B[97m"
874
+ };
875
+ LEVEL_COLORS = {
876
+ [0 /* SILENT */]: ANSI.dim,
877
+ [1 /* ERROR */]: ANSI.red,
878
+ [2 /* WARN */]: ANSI.yellow,
879
+ [3 /* INFO */]: ANSI.blue,
880
+ [4 /* DEBUG */]: ANSI.magenta,
881
+ [5 /* TRACE */]: ANSI.dim
882
+ };
883
+ LEVEL_ICONS = {
884
+ [0 /* SILENT */]: "",
885
+ [1 /* ERROR */]: "?",
886
+ [2 /* WARN */]: "?",
887
+ [3 /* INFO */]: "?",
888
+ [4 /* DEBUG */]: "o",
889
+ [5 /* TRACE */]: "o"
890
+ };
891
+ }
892
+ });
893
+
894
+ // ../../packages/shared/tw/logger/transports.ts
895
+ var init_transports = __esm({
896
+ "../../packages/shared/tw/logger/transports.ts"() {
897
+ init_colors();
898
+ }
899
+ });
900
+
901
+ // ../../packages/shared/tw/logger/logger.ts
902
+ var init_logger = __esm({
903
+ "../../packages/shared/tw/logger/logger.ts"() {
904
+ init_levels();
905
+ init_transports();
906
+ }
907
+ });
908
+
909
+ // ../../packages/shared/tw/logger/progress.ts
910
+ var init_progress = __esm({
911
+ "../../packages/shared/tw/logger/progress.ts"() {
912
+ init_logger();
913
+ init_colors();
914
+ }
915
+ });
916
+
917
+ // ../../packages/shared/tw/logger/index.ts
918
+ var init_logger2 = __esm({
919
+ "../../packages/shared/tw/logger/index.ts"() {
920
+ init_colors();
921
+ init_levels();
922
+ init_logger();
923
+ init_progress();
924
+ init_transports();
925
+ }
926
+ });
927
+
928
+ // ../../packages/shared/tw/config/validate.ts
929
+ var init_validate = __esm({
930
+ "../../packages/shared/tw/config/validate.ts"() {
931
+ init_logger2();
932
+ }
933
+ });
934
+
935
+ // ../../packages/shared/tw/config/loader.ts
936
+ var init_loader = __esm({
937
+ "../../packages/shared/tw/config/loader.ts"() {
938
+ init_defaults();
939
+ init_fs();
940
+ init_logger2();
941
+ init_validate();
942
+ }
943
+ });
944
+
945
+ // ../../packages/shared/tw/config/index.ts
946
+ var init_config = __esm({
947
+ "../../packages/shared/tw/config/index.ts"() {
948
+ init_defaults();
949
+ init_loader();
950
+ init_validate();
951
+ }
952
+ });
953
+
954
+ // ../../packages/shared/tw/cache.ts
955
+ function parseCacheBody(body) {
956
+ const meta = {};
957
+ const pairRegex = /\b(life|tag|revalidate|stale|expire)\s*=?\s*(?:"([^"]*)"|'([^']*)'|(\d+))/g;
958
+ let m;
959
+ while ((m = pairRegex.exec(body)) !== null) {
960
+ const key = m[1];
961
+ const strVal = m[2] ?? m[3] ?? null;
962
+ const numVal = m[4] != null ? Number(m[4]) : null;
963
+ if (key === "revalidate" || key === "stale" || key === "expire") {
964
+ if (numVal != null && Number.isFinite(numVal) && numVal >= 0) {
965
+ meta[key] = numVal;
966
+ }
967
+ } else if (key === "life" || key === "tag") {
968
+ if (strVal != null && strVal.length > 0) {
969
+ meta[key] = strVal;
970
+ }
735
971
  }
736
- const acceptLc = (accept ?? "").toLowerCase();
737
- for (const f of config.formats) {
738
- if (acceptLc.includes(`image/${f}`)) return f === "jpeg" ? "jpeg" : f;
972
+ }
973
+ return meta;
974
+ }
975
+ function extractCacheDirective(source) {
976
+ const m = /page\s*\{[^}]*?\bcache\s*\{([^}]*)\}/.exec(source);
977
+ if (!m) return null;
978
+ return parseCacheBody(m[1]);
979
+ }
980
+ function cacheMetaIsUsable(meta) {
981
+ if (!meta) return false;
982
+ return meta.revalidate != null || meta.stale != null || meta.expire != null || !!meta.life;
983
+ }
984
+ function resolveCache(meta, profiles, opts) {
985
+ if (!cacheMetaIsUsable(meta)) return null;
986
+ let revalidate = meta.revalidate;
987
+ let stale = meta.stale ?? 0;
988
+ let expire = meta.expire;
989
+ if (meta.life) {
990
+ const profile = (profiles && profiles[meta.life]) ?? BUILTIN_CACHE_PROFILES[meta.life];
991
+ if (!profile) {
992
+ throw new Error(
993
+ 'TW092: Unknown cache profile "' + meta.life + '". Add it to tw.config.ts cache.profiles or use one of: ' + Object.keys(BUILTIN_CACHE_PROFILES).join(", ") + "."
994
+ );
739
995
  }
740
- return null;
996
+ if (revalidate == null && profile.revalidate != null) revalidate = profile.revalidate;
997
+ if (meta.stale == null && profile.stale != null) stale = profile.stale;
998
+ if (expire == null && profile.expire != null) expire = profile.expire;
741
999
  }
742
- function isRemoteSource(src) {
743
- return /^https?:\/\//i.test(src);
1000
+ if (revalidate == null || !Number.isFinite(revalidate) || revalidate <= 0) return null;
1001
+ if (expire == null || !Number.isFinite(expire)) {
1002
+ expire = opts?.legacy ? Infinity : revalidate;
744
1003
  }
745
- function remoteAllowed(url) {
746
- if (!config.remoteAllowHosts.includes(url.hostname)) return false;
747
- if (url.username || url.password) return false;
748
- return url.protocol === "https:" || url.protocol === "http:";
1004
+ if (expire < revalidate) expire = revalidate;
1005
+ return { revalidate, stale, expire, tag: meta.tag };
1006
+ }
1007
+ function canonicalizeQuery(search) {
1008
+ if (!search) return "";
1009
+ const q = search.startsWith("?") ? search.slice(1) : search;
1010
+ if (!q) return "";
1011
+ const pairs = [];
1012
+ for (const part of q.split("&")) {
1013
+ if (!part) continue;
1014
+ const i = part.indexOf("=");
1015
+ let k, v;
1016
+ if (i < 0) {
1017
+ k = part;
1018
+ v = "";
1019
+ } else {
1020
+ k = part.slice(0, i);
1021
+ v = part.slice(i + 1);
1022
+ }
1023
+ if (v === "") continue;
1024
+ try {
1025
+ k = decodeURIComponent(k);
1026
+ } catch {
1027
+ }
1028
+ try {
1029
+ v = decodeURIComponent(v);
1030
+ } catch {
1031
+ }
1032
+ pairs.push([k, v]);
749
1033
  }
750
- function isPrivateAddress(host) {
751
- const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
752
- if (m) {
753
- const a = Number(m[1]), b = Number(m[2]);
754
- if (a === 0 || a === 10 || a === 127) return true;
755
- if (a === 169 && b === 254) return true;
756
- if (a === 172 && b >= 16 && b <= 31) return true;
757
- if (a === 192 && b === 168) return true;
758
- if (a >= 224) return true;
759
- return false;
1034
+ pairs.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
1035
+ return pairs.map(([k, v]) => encodeURIComponent(k) + "=" + encodeURIComponent(v)).join("&");
1036
+ }
1037
+ function balancedEnd(source, i) {
1038
+ let depth = 0;
1039
+ let inStr = null;
1040
+ for (let k = i; k < source.length; k++) {
1041
+ const ch = source[k];
1042
+ if (inStr) {
1043
+ if (ch === "\\") k++;
1044
+ else if (ch === inStr) inStr = null;
1045
+ continue;
1046
+ }
1047
+ if (ch === '"' || ch === "'" || ch === "`") {
1048
+ inStr = ch;
1049
+ continue;
1050
+ }
1051
+ if (ch === "{") depth++;
1052
+ else if (ch === "}") {
1053
+ depth--;
1054
+ if (depth === 0) return k;
760
1055
  }
761
- const h = host.toLowerCase();
762
- if (h === "::" || h === "::1") return true;
763
- if (h.startsWith("fe80:")) return true;
764
- if (h.startsWith("fc") || h.startsWith("fd")) return true;
765
- if (h.startsWith("::ffff:")) return isPrivateAddress(h.slice(7));
766
- return false;
767
1056
  }
768
- async function safeFetchRemote(src) {
769
- let url;
1057
+ return -1;
1058
+ }
1059
+ function readCacheProfilesSync(rootDir) {
1060
+ try {
1061
+ let read;
770
1062
  try {
771
- url = new URL(src);
1063
+ read = globalThis.require("node:fs").readFileSync;
772
1064
  } catch {
773
- return null;
1065
+ const fsmod = globalThis.process ? globalThis.process.getBuiltinModule?.("node:fs") : null;
1066
+ if (!fsmod) return {};
1067
+ read = fsmod.readFileSync;
774
1068
  }
775
- for (let hop = 0; hop < 3; hop++) {
776
- if (!remoteAllowed(url)) return null;
777
- try {
778
- const dns = await import("node:dns");
779
- const addrs = await dns.promises.lookup(url.hostname, { all: true });
780
- if (!addrs.length) return null;
781
- for (const a of addrs) {
782
- if (isPrivateAddress(a.address)) return null;
783
- }
784
- } catch {
785
- return null;
786
- }
787
- let res;
1069
+ let src = "";
1070
+ for (const name of ["tw.config.ts", "tw.config.js", "tw.config.mjs"]) {
788
1071
  try {
789
- res = await fetch(url, { redirect: "manual" });
1072
+ src = read(rootDir.endsWith("/") ? rootDir + name : rootDir + "/" + name, "utf-8");
1073
+ if (src) break;
790
1074
  } catch {
791
- return null;
792
- }
793
- if (res.status >= 300 && res.status < 400) {
794
- const loc = res.headers.get("location");
795
- if (!loc) return null;
796
- try {
797
- url = new URL(loc, url);
798
- } catch {
799
- return null;
800
- }
801
- continue;
802
1075
  }
803
- if (!res.ok) return null;
804
- return Buffer.from(await res.arrayBuffer());
805
1076
  }
806
- return null;
1077
+ if (!src) return {};
1078
+ return parseCacheProfilesSource(src);
1079
+ } catch {
1080
+ return {};
807
1081
  }
808
- async function readSource(src) {
809
- if (isRemoteSource(src)) {
810
- const data = await safeFetchRemote(src);
811
- return data ? { data, mtime: 0 } : null;
812
- }
813
- let decoded;
814
- try {
815
- decoded = decodeURIComponent(src);
816
- } catch {
817
- return null;
818
- }
819
- const clean = (0, import_node_path2.normalize)(decoded).replace(/\\/g, "/");
820
- if (clean.includes("\0") || clean.includes("..")) return null;
821
- const file = (0, import_node_path2.resolve)(sourceDir, "." + (clean.startsWith("/") ? clean : "/" + clean));
822
- if (!file.startsWith(sourceDir + "/") && file !== sourceDir) return null;
823
- const dot = clean.split("/").pop() ?? "";
824
- if (dot.startsWith(".")) return null;
825
- if (!(0, import_node_fs.existsSync)(file)) return null;
826
- const st = (0, import_node_fs.statSync)(file);
827
- if (!st.isFile()) return null;
828
- return { data: (0, import_node_fs.readFileSync)(file), mtime: st.mtimeMs };
829
- }
830
- function cacheKey(src, w, h, q, f, mtime) {
831
- return (0, import_node_crypto2.createHash)("sha256").update(`${src}|${mtime}|${w}|${h}|${q}|${f}`).digest("hex").slice(0, 32);
832
- }
833
- async function processBuffer(data, w, h, q, format) {
834
- const sharp = await loadSharp();
835
- if (sharp && format) {
836
- try {
837
- let img = sharp(data);
838
- if (w || h) {
839
- img = img.resize(w || void 0, h || void 0, {
840
- fit: "inside",
841
- withoutEnlargement: true
842
- });
843
- }
844
- if (format !== "jpeg" && format !== "png") {
845
- img = img.toFormat(format, { quality: q });
846
- } else if (format === "jpeg") {
847
- img = img.jpeg({ quality: q });
848
- }
849
- const out = await img.toBuffer();
850
- return { data: out, contentType: `image/${format}`, viaSharp: true };
851
- } catch (err2) {
852
- console.warn(` [tw/image] processing failed (${format}, ${w}x${h}), serving original: ${err2?.message ?? err2}`);
853
- return { data, contentType: "", viaSharp: false };
854
- }
855
- }
856
- if (!warnedNoSharp) {
857
- warnedNoSharp = true;
858
- console.warn(" [tw/image] sharp not installed \u2014 serving original images (bun add sharp to enable resizing/WebP/AVIF)");
859
- }
860
- return { data, contentType: "", viaSharp: false };
861
- }
862
- async function handle(request) {
863
- const url = new URL(request.url);
864
- const pathname = url.pathname;
865
- if (!pathname.startsWith("/_tw/img/") && pathname !== "/_tw/img") return null;
866
- if (request.method !== "GET" && request.method !== "HEAD") {
867
- return new Response("Method Not Allowed", { status: 405 });
868
- }
869
- const q = url.searchParams;
870
- let src;
871
- if (pathname === "/_tw/img/_remote") {
872
- src = q.get("src") ?? "";
873
- if (!src) return new Response("Not Found", { status: 404 });
874
- } else {
875
- src = pathname.slice("/_tw/img".length) || "/";
876
- if (src === "/" || src === "") return new Response("Not Found", { status: 404 });
877
- }
878
- const w = Math.max(0, Math.min(1e4, parseInt(q.get("w") ?? "0", 10) || 0));
879
- const h = Math.max(0, Math.min(1e4, parseInt(q.get("h") ?? "0", 10) || 0));
880
- const quality = Math.max(1, Math.min(100, parseInt(q.get("q") ?? String(config.quality), 10) || config.quality));
881
- const format = negotiateFormat(q.get("f"), request.headers.get("accept") ?? "");
882
- const source = await readSource(src);
883
- if (!source) return new Response("Not Found", { status: 404 });
884
- if ((0, import_node_path2.extname)(src).toLowerCase() === ".svg") {
885
- return new Response(new Uint8Array(source.data), {
886
- status: 200,
887
- headers: {
888
- "Content-Type": "image/svg+xml",
889
- "Cache-Control": `public, max-age=${config.cacheMaxAge}`,
890
- "X-TW-Image": "svg-passthrough"
891
- }
892
- });
893
- }
894
- if (!w && !h && !format) {
895
- return new Response(new Uint8Array(source.data), {
896
- status: 200,
897
- headers: {
898
- "Content-Type": contentTypeFor(src),
899
- "Cache-Control": `public, max-age=${config.cacheMaxAge}`,
900
- "X-TW-Image": "original"
901
- }
902
- });
903
- }
904
- const key = cacheKey(src, w, h, quality, format ?? "orig", source.mtime);
905
- const cacheFile = (0, import_node_path2.join)(cacheDir, `${key}${format ? "." + format : ""}`);
906
- const etag = `"${key}"`;
907
- if (request.headers.get("if-none-match") === etag) {
908
- return new Response(null, { status: 304, headers: { ETag: etag } });
909
- }
910
- if ((0, import_node_fs.existsSync)(cacheFile)) {
911
- const cached = (0, import_node_fs.readFileSync)(cacheFile);
912
- return new Response(new Uint8Array(cached), {
913
- status: 200,
914
- headers: {
915
- "Content-Type": contentTypeFor(cacheFile),
916
- "Cache-Control": `public, max-age=${config.cacheMaxAge}`,
917
- ETag: etag,
918
- "X-TW-Image": "cache"
919
- }
920
- });
921
- }
922
- const result = await processBuffer(source.data, w, h, quality, format);
923
- if (result.viaSharp) {
924
- try {
925
- (0, import_node_fs.mkdirSync)(cacheDir, { recursive: true });
926
- (0, import_node_fs.writeFileSync)(cacheFile, result.data);
927
- } catch {
928
- }
929
- }
930
- return new Response(new Uint8Array(result.data), {
931
- status: 200,
932
- headers: {
933
- "Content-Type": result.contentType || contentTypeFor(src),
934
- "Cache-Control": `public, max-age=${config.cacheMaxAge}`,
935
- ETag: etag,
936
- "X-TW-Image": result.viaSharp ? "optimized" : "passthrough"
937
- }
938
- });
939
- }
940
- return { handle, sourceDir, cacheDir, config };
941
- }
942
- var import_node_crypto2, import_node_fs, import_node_path2, import_node_url2, DEFAULT_IMAGE_CONFIG, IMAGE_EXTENSIONS, SHARP_FORMATS;
943
- var init_tw = __esm({
944
- "../../packages/image/tw/index.ts"() {
945
- import_node_crypto2 = require("node:crypto");
946
- import_node_fs = require("node:fs");
947
- import_node_path2 = require("node:path");
948
- import_node_url2 = require("node:url");
949
- DEFAULT_IMAGE_CONFIG = {
950
- quality: 75,
951
- formats: ["avif", "webp"],
952
- breakpoints: [320, 640, 768, 1024, 1280, 1920],
953
- remoteAllowHosts: [],
954
- cacheMaxAge: 86400
955
- };
956
- IMAGE_EXTENSIONS = {
957
- ".jpg": "image/jpeg",
958
- ".jpeg": "image/jpeg",
959
- ".png": "image/png",
960
- ".webp": "image/webp",
961
- ".avif": "image/avif",
962
- ".gif": "image/gif",
963
- ".svg": "image/svg+xml",
964
- ".ico": "image/x-icon"
965
- };
966
- SHARP_FORMATS = {
967
- avif: "avif",
968
- webp: "webp",
969
- jpeg: "jpeg",
970
- jpg: "jpeg",
971
- png: "png"
972
- };
973
- }
974
- });
975
-
976
- // ../../packages/server/tw/websocket-manager.ts
977
- var DEFAULT_MAX_ROOMS, DEFAULT_MAX_ROOM_SIZE, WebSocketManager;
978
- var init_websocket_manager = __esm({
979
- "../../packages/server/tw/websocket-manager.ts"() {
980
- DEFAULT_MAX_ROOMS = 1e3;
981
- DEFAULT_MAX_ROOM_SIZE = 1e4;
982
- WebSocketManager = class {
983
- connections = /* @__PURE__ */ new Map();
984
- meta = /* @__PURE__ */ new Map();
985
- rooms = /* @__PURE__ */ new Map();
986
- nextId = 1;
987
- maxRooms;
988
- maxRoomSize;
989
- constructor(options = {}) {
990
- this.maxRooms = options.maxRooms ?? DEFAULT_MAX_ROOMS;
991
- this.maxRoomSize = options.maxRoomSize ?? DEFAULT_MAX_ROOM_SIZE;
992
- }
993
- /** Register a socket on open; returns the assigned connection id. */
994
- add(connection, meta = {}) {
995
- let id = connection?.data?.id ?? connection?.id;
996
- if (!id) {
997
- id = "ws-" + process.pid + "-" + this.nextId++;
998
- }
999
- if (connection && typeof connection === "object") {
1000
- if (!connection.data) connection.data = { id };
1001
- else if (!connection.data.id) connection.data.id = id;
1002
- if (!connection.id) connection.id = id;
1003
- }
1004
- this.connections.set(id, connection);
1005
- this.meta.set(id, meta);
1006
- return id;
1007
- }
1008
- /** Lookup by connection id. */
1009
- get(id) {
1010
- return this.connections.get(id);
1011
- }
1012
- /** Deregister on close; auto-leaves rooms (last member out kills the room). */
1013
- remove(id) {
1014
- for (const room of this.rooms.values()) {
1015
- room.members.delete(id);
1016
- if (room.members.size === 0) this.rooms.delete(room.name);
1017
- }
1018
- this.meta.delete(id);
1019
- return this.connections.delete(id);
1020
- }
1021
- /** Serialize once and send to every socket regardless of room membership. */
1022
- broadcast(payload) {
1023
- const data = typeof payload === "string" ? payload : JSON.stringify(payload);
1024
- for (const conn of this.connections.values()) {
1025
- this.safeSend(conn, data);
1026
- }
1027
- }
1028
- /** Send to one connection. */
1029
- send(id, payload) {
1030
- const conn = this.connections.get(id);
1031
- if (!conn) return false;
1032
- this.safeSend(conn, typeof payload === "string" ? payload : JSON.stringify(payload));
1033
- return true;
1034
- }
1035
- /** All live entries. */
1036
- getConnections() {
1037
- return [...this.connections.keys()].map((id) => ({ id, meta: this.meta.get(id) ?? {} }));
1038
- }
1039
- /** Explicit pre-creation with metadata (rooms also auto-create on join). */
1040
- createRoom(name, metadata = {}) {
1041
- if (this.rooms.has(name)) return;
1042
- if (this.rooms.size >= this.maxRooms) {
1043
- throw new Error(`maxRooms reached (${this.maxRooms}): cannot create "${name}"`);
1044
- }
1045
- this.rooms.set(name, { name, metadata, members: /* @__PURE__ */ new Set() });
1046
- }
1047
- /** Join a room (auto-creates). A full room refuses joins. */
1048
- joinRoom(connectionId, name) {
1049
- if (!this.connections.has(connectionId)) return false;
1050
- let room = this.rooms.get(name);
1051
- if (!room) {
1052
- if (this.rooms.size >= this.maxRooms) {
1053
- throw new Error(`maxRooms reached (${this.maxRooms}): cannot create "${name}"`);
1054
- }
1055
- room = { name, metadata: {}, members: /* @__PURE__ */ new Set() };
1056
- this.rooms.set(name, room);
1057
- }
1058
- if (room.members.size >= this.maxRoomSize) return false;
1059
- room.members.add(connectionId);
1060
- return true;
1061
- }
1062
- /** Leave a room; the room disappears when its last member leaves. */
1063
- leaveRoom(connectionId, name) {
1064
- const room = this.rooms.get(name);
1065
- if (!room) return false;
1066
- const had = room.members.delete(connectionId);
1067
- if (room.members.size === 0) this.rooms.delete(name);
1068
- return had;
1069
- }
1070
- /** Iterate only that room's members — Set membership guarantees exactly one copy. */
1071
- broadcastToRoom(name, payload) {
1072
- const room = this.rooms.get(name);
1073
- if (!room) return 0;
1074
- const data = typeof payload === "string" ? payload : JSON.stringify(payload);
1075
- let sent = 0;
1076
- for (const id of room.members) {
1077
- const conn = this.connections.get(id);
1078
- if (this.safeSend(conn, data)) sent++;
1079
- }
1080
- return sent;
1081
- }
1082
- /** Room + members. */
1083
- getRoom(name) {
1084
- const room = this.rooms.get(name);
1085
- if (!room) return void 0;
1086
- return { name: room.name, metadata: room.metadata, members: [...room.members] };
1087
- }
1088
- /** All rooms. */
1089
- getRooms() {
1090
- return [...this.rooms.values()].map((r) => ({ name: r.name, members: r.members.size }));
1091
- }
1092
- safeSend(conn, data) {
1093
- try {
1094
- if (!conn || typeof conn?.send !== "function") return false;
1095
- if (typeof conn.readyState === "number" && conn.readyState !== 1) return false;
1096
- conn.send(data);
1097
- return true;
1098
- } catch {
1099
- return false;
1100
- }
1101
- }
1102
- };
1103
- }
1104
- });
1105
-
1106
- // ../../packages/server/tw/routing/layout-chain.ts
1107
- function resolveLayoutChain(matchResult) {
1108
- const { node, chain, params } = matchResult;
1109
- const layouts = [];
1110
- const templates = [];
1111
- for (const nodeInChain of chain) {
1112
- const layoutFile = nodeInChain.files.find((f) => f.type === "layout");
1113
- if (layoutFile) layouts.push(layoutFile);
1114
- const templateFile = nodeInChain.files.find((f) => f.type === "template");
1115
- if (templateFile) templates.push(templateFile);
1116
- }
1117
- let loading;
1118
- let error;
1119
- let notFound2;
1120
- let head;
1121
- for (let i = chain.length - 1; i >= 0; i--) {
1122
- if (!loading) {
1123
- loading = chain[i].files.find((f) => f.type === "loading");
1124
- }
1125
- if (!error) {
1126
- error = chain[i].files.find((f) => f.type === "error");
1127
- }
1128
- if (!notFound2) {
1129
- notFound2 = chain[i].files.find((f) => f.type === "not-found");
1130
- }
1131
- if (!head) {
1132
- head = chain[i].files.find((f) => f.type === "head");
1133
- }
1134
- }
1135
- const page = node.files.find((f) => f.type === "page");
1136
- let renderMode = "ssr";
1137
- if (page) {
1138
- renderMode = extractRenderMode(page);
1139
- }
1140
- return {
1141
- node,
1142
- chain,
1143
- params,
1144
- layouts,
1145
- templates,
1146
- loading,
1147
- error,
1148
- notFound: notFound2,
1149
- head,
1150
- page,
1151
- middleware: [],
1152
- renderMode
1153
- };
1154
- }
1155
- function findGlobalError(rootNode) {
1156
- return rootNode.files.find((f) => f.type === "global-error");
1157
- }
1158
- function findRootError(rootNode) {
1159
- return (rootNode.files || []).find((f) => f.type === "error");
1160
- }
1161
- function findRootNotFound(rootNode) {
1162
- return rootNode.files.find((f) => f.type === "not-found");
1163
- }
1164
- function collectParallelDefaults(node) {
1165
- const defaults = /* @__PURE__ */ new Map();
1166
- for (const child of node.children) {
1167
- if (child.kind === "parallel" && child.slotName) {
1168
- const defaultFile = child.files.find((f) => f.type === "default");
1169
- if (defaultFile) {
1170
- defaults.set(child.slotName, defaultFile);
1171
- }
1172
- }
1173
- }
1174
- return defaults;
1175
1082
  }
1176
- function collectParallelPages(node, pathParts, index) {
1177
- const pages = /* @__PURE__ */ new Map();
1178
- for (const child of node.children) {
1179
- if (child.kind === "parallel" && child.slotName) {
1180
- const pageFile = child.files.find((f) => f.type === "page");
1181
- if (pageFile) {
1182
- pages.set(child.slotName, pageFile);
1183
- }
1184
- if (index < pathParts.length) {
1185
- for (const grandchild of child.children) {
1186
- if (grandchild.kind === "static" && grandchild.segment === pathParts[index]) {
1187
- const deepPage = grandchild.files.find((f) => f.type === "page");
1188
- if (deepPage) {
1189
- pages.set(child.slotName, deepPage);
1190
- }
1191
- }
1192
- }
1193
- }
1194
- }
1195
- }
1196
- return pages;
1197
- }
1198
- function extractRenderMode(page) {
1199
- try {
1200
- const fs = require("node:fs");
1201
- const source = fs.readFileSync(page.absolutePath, "utf-8");
1202
- const m = source.match(/render\s+(static|ssr|island|edge)/);
1203
- if (m) {
1204
- return m[1];
1205
- }
1206
- } catch {
1207
- }
1208
- return "ssr";
1209
- }
1210
- var init_layout_chain = __esm({
1211
- "../../packages/server/tw/routing/layout-chain.ts"() {
1212
- }
1213
- });
1214
-
1215
- // ../../packages/shared/tw/hashing/md5.ts
1216
- var init_md5 = __esm({
1217
- "../../packages/shared/tw/hashing/md5.ts"() {
1218
- }
1219
- });
1220
-
1221
- // ../../packages/shared/tw/hashing/crc32.ts
1222
- var crc32Table;
1223
- var init_crc32 = __esm({
1224
- "../../packages/shared/tw/hashing/crc32.ts"() {
1225
- crc32Table = (() => {
1226
- const table = new Uint32Array(256);
1227
- for (let i = 0; i < 256; i++) {
1228
- let crc = i;
1229
- for (let j = 0; j < 8; j++) {
1230
- crc = crc & 1 ? 3988292384 ^ crc >>> 1 : crc >>> 1;
1231
- }
1232
- table[i] = crc >>> 0;
1233
- }
1234
- return table;
1235
- })();
1236
- }
1237
- });
1238
-
1239
- // ../../packages/shared/tw/hashing/sha.ts
1240
- function syncHashFallback(algorithm, data) {
1241
- const input = typeof data === "string" ? data : new TextDecoder().decode(data);
1242
- let hash2 = 2166136261;
1243
- for (let i = 0; i < input.length; i++) {
1244
- hash2 ^= input.charCodeAt(i);
1245
- hash2 = Math.imul(hash2, 16777619);
1246
- }
1247
- return (hash2 >>> 0).toString(16).padStart(8, "0").repeat(8);
1248
- }
1249
- function sha256(data) {
1250
- try {
1251
- const hasher = new BunHashSync("sha256");
1252
- return hasher.update(data).digest("hex");
1253
- } catch {
1254
- return syncHashFallback("sha256", data);
1083
+ function parseCacheProfilesSource(src) {
1084
+ const out = {};
1085
+ const cleaned = src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*/gm, "");
1086
+ const cacheKey = /\bcache\s*:\s*\{/.exec(cleaned);
1087
+ if (!cacheKey) return out;
1088
+ const cacheOpen = cleaned.indexOf("{", cacheKey.index);
1089
+ const cacheClose = balancedEnd(cleaned, cacheOpen);
1090
+ if (cacheClose < 0) return out;
1091
+ const cacheBody = cleaned.slice(cacheOpen + 1, cacheClose);
1092
+ const profKey = /\bprofiles\s*:\s*\{/.exec(cacheBody);
1093
+ if (!profKey) return out;
1094
+ const profOpen = cacheBody.indexOf("{", profKey.index);
1095
+ const profClose = balancedEnd(cacheBody, profOpen);
1096
+ if (profClose < 0) return out;
1097
+ const profBody = cacheBody.slice(profOpen + 1, profClose);
1098
+ for (const m of profBody.matchAll(/([A-Za-z0-9_]+)\s*:\s*\{([^}]*)\}/g)) {
1099
+ const profile = {};
1100
+ for (const kv of m[2].matchAll(/(stale|revalidate|expire)\s*:\s*(\d+)/g)) {
1101
+ profile[kv[1]] = Number(kv[2]);
1102
+ }
1103
+ if (Object.keys(profile).length > 0) out[m[1]] = profile;
1255
1104
  }
1105
+ return out;
1256
1106
  }
1257
- var BunHashSync;
1258
- var init_sha = __esm({
1259
- "../../packages/shared/tw/hashing/sha.ts"() {
1260
- init_md5();
1261
- init_crc32();
1262
- BunHashSync = class {
1263
- hasher;
1264
- constructor(algorithm) {
1265
- const nodeCrypto = globalThis.require?.("node:crypto") || globalThis.crypto;
1266
- if (nodeCrypto?.createHash) {
1267
- this.hasher = nodeCrypto.createHash(algorithm);
1268
- } else {
1269
- throw new Error("No sync hash available");
1270
- }
1271
- }
1272
- update(data) {
1273
- const input = typeof data === "string" ? data : Buffer.from(data);
1274
- this.hasher.update(input);
1275
- return this;
1276
- }
1277
- digest(encoding) {
1278
- return this.hasher.digest(encoding);
1279
- }
1280
- };
1281
- }
1282
- });
1283
-
1284
- // ../../packages/shared/tw/utils.ts
1285
- var init_utils = __esm({
1286
- "../../packages/shared/tw/utils.ts"() {
1287
- }
1288
- });
1289
-
1290
- // ../../packages/shared/tw/fs/runtime-compat.ts
1291
- var init_runtime_compat = __esm({
1292
- "../../packages/shared/tw/fs/runtime-compat.ts"() {
1293
- }
1294
- });
1295
-
1296
- // ../../packages/shared/tw/constants/misc.ts
1297
- var MIME_TYPES, VLQ_BASE_SHIFT, VLQ_BASE, VLQ_BASE_MASK;
1298
- var init_misc = __esm({
1299
- "../../packages/shared/tw/constants/misc.ts"() {
1300
- MIME_TYPES = {
1301
- ".html": "text/html",
1302
- ".htm": "text/html",
1303
- ".css": "text/css",
1304
- ".js": "text/javascript",
1305
- ".mjs": "text/javascript",
1306
- ".json": "application/json",
1307
- ".xml": "application/xml",
1308
- ".txt": "text/plain",
1309
- ".md": "text/markdown",
1310
- ".csv": "text/csv",
1311
- ".svg": "image/svg+xml",
1312
- ".png": "image/png",
1313
- ".jpg": "image/jpeg",
1314
- ".jpeg": "image/jpeg",
1315
- ".gif": "image/gif",
1316
- ".webp": "image/webp",
1317
- ".avif": "image/avif",
1318
- ".ico": "image/x-icon",
1319
- ".bmp": "image/bmp",
1320
- ".woff": "font/woff",
1321
- ".woff2": "font/woff2",
1322
- ".ttf": "font/ttf",
1323
- ".otf": "font/otf",
1324
- ".pdf": "application/pdf",
1325
- ".zip": "application/zip",
1326
- ".gz": "application/gzip",
1327
- ".tar": "application/x-tar",
1328
- ".mp4": "video/mp4",
1329
- ".webm": "video/webm",
1330
- ".mp3": "audio/mpeg",
1331
- ".ogg": "audio/ogg",
1332
- ".wav": "audio/wav",
1333
- ".wasm": "application/wasm",
1334
- ".tw": "text/tw",
1335
- ".tss": "text/tss",
1336
- ".toml": "application/toml",
1337
- ".yaml": "application/yaml",
1338
- ".yml": "application/yaml",
1339
- ".webmanifest": "application/manifest+json"
1340
- };
1341
- VLQ_BASE_SHIFT = 5;
1342
- VLQ_BASE = 1 << VLQ_BASE_SHIFT;
1343
- VLQ_BASE_MASK = VLQ_BASE - 1;
1344
- }
1345
- });
1346
-
1347
- // ../../packages/shared/tw/fs/async.ts
1348
- var init_async = __esm({
1349
- "../../packages/shared/tw/fs/async.ts"() {
1350
- init_runtime_compat();
1351
- init_runtime_compat();
1352
- init_misc();
1353
- }
1354
- });
1355
-
1356
- // ../../packages/runtime/tw/utils/path-utils.ts
1357
- var init_path_utils = __esm({
1358
- "../../packages/runtime/tw/utils/path-utils.ts"() {
1359
- }
1360
- });
1361
-
1362
- // ../../packages/shared/tw/fs/glob.ts
1363
- var init_glob = __esm({
1364
- "../../packages/shared/tw/fs/glob.ts"() {
1365
- init_runtime_compat();
1366
- init_path_utils();
1367
- }
1368
- });
1369
-
1370
- // ../../packages/shared/tw/fs/sync.ts
1371
- var init_sync = __esm({
1372
- "../../packages/shared/tw/fs/sync.ts"() {
1373
- init_runtime_compat();
1374
- init_runtime_compat();
1375
- init_async();
1376
- }
1377
- });
1378
-
1379
- // ../../packages/shared/tw/fs/watch.ts
1380
- var init_watch = __esm({
1381
- "../../packages/shared/tw/fs/watch.ts"() {
1382
- init_sync();
1383
- init_runtime_compat();
1384
- }
1385
- });
1386
-
1387
- // ../../packages/shared/tw/fs/index.ts
1388
- var init_fs = __esm({
1389
- "../../packages/shared/tw/fs/index.ts"() {
1390
- init_async();
1391
- init_glob();
1392
- init_sync();
1393
- init_watch();
1394
- }
1395
- });
1396
-
1397
- // ../../packages/shared/tw/config/defaults.ts
1398
- var init_defaults = __esm({
1399
- "../../packages/shared/tw/config/defaults.ts"() {
1400
- init_fs();
1401
- }
1402
- });
1403
-
1404
- // ../../packages/shared/tw/logger/levels.ts
1405
- var init_levels = __esm({
1406
- "../../packages/shared/tw/logger/levels.ts"() {
1407
- }
1408
- });
1409
-
1410
- // ../../packages/shared/tw/logger/colors.ts
1411
- var ANSI, LEVEL_COLORS, LEVEL_ICONS;
1412
- var init_colors = __esm({
1413
- "../../packages/shared/tw/logger/colors.ts"() {
1414
- init_levels();
1415
- ANSI = {
1416
- reset: "\x1B[0m",
1417
- bold: "\x1B[1m",
1418
- dim: "\x1B[2m",
1419
- italic: "\x1B[3m",
1420
- underline: "\x1B[4m",
1421
- blink: "\x1B[5m",
1422
- inverse: "\x1B[7m",
1423
- hidden: "\x1B[8m",
1424
- strikethrough: "\x1B[9m",
1425
- black: "\x1B[30m",
1426
- red: "\x1B[31m",
1427
- green: "\x1B[32m",
1428
- yellow: "\x1B[33m",
1429
- blue: "\x1B[34m",
1430
- magenta: "\x1B[35m",
1431
- cyan: "\x1B[36m",
1432
- white: "\x1B[37m",
1433
- bgBlack: "\x1B[40m",
1434
- bgRed: "\x1B[41m",
1435
- bgGreen: "\x1B[42m",
1436
- bgYellow: "\x1B[44m",
1437
- bgBlue: "\x1B[44m",
1438
- bgMagenta: "\x1B[45m",
1439
- bgCyan: "\x1B[46m",
1440
- bgWhite: "\x1B[47m",
1441
- brightBlack: "\x1B[90m",
1442
- brightRed: "\x1B[91m",
1443
- brightGreen: "\x1B[92m",
1444
- brightYellow: "\x1B[93m",
1445
- brightBlue: "\x1B[94m",
1446
- brightMagenta: "\x1B[95m",
1447
- brightCyan: "\x1B[96m",
1448
- brightWhite: "\x1B[97m"
1449
- };
1450
- LEVEL_COLORS = {
1451
- [0 /* SILENT */]: ANSI.dim,
1452
- [1 /* ERROR */]: ANSI.red,
1453
- [2 /* WARN */]: ANSI.yellow,
1454
- [3 /* INFO */]: ANSI.blue,
1455
- [4 /* DEBUG */]: ANSI.magenta,
1456
- [5 /* TRACE */]: ANSI.dim
1457
- };
1458
- LEVEL_ICONS = {
1459
- [0 /* SILENT */]: "",
1460
- [1 /* ERROR */]: "?",
1461
- [2 /* WARN */]: "?",
1462
- [3 /* INFO */]: "?",
1463
- [4 /* DEBUG */]: "o",
1464
- [5 /* TRACE */]: "o"
1107
+ var BUILTIN_CACHE_PROFILES;
1108
+ var init_cache = __esm({
1109
+ "../../packages/shared/tw/cache.ts"() {
1110
+ BUILTIN_CACHE_PROFILES = {
1111
+ seconds: { stale: 1, revalidate: 1, expire: 2 },
1112
+ minutes: { stale: 60, revalidate: 300, expire: 3600 },
1113
+ hours: { stale: 3600, revalidate: 14400, expire: 86400 },
1114
+ days: { stale: 86400, revalidate: 604800, expire: 2592e3 },
1115
+ max: { stale: 2592e3, revalidate: 5184e3, expire: 31536e3 }
1465
1116
  };
1466
1117
  }
1467
1118
  });
1468
1119
 
1469
- // ../../packages/shared/tw/logger/transports.ts
1470
- var init_transports = __esm({
1471
- "../../packages/shared/tw/logger/transports.ts"() {
1472
- init_colors();
1473
- }
1474
- });
1475
-
1476
- // ../../packages/shared/tw/logger/logger.ts
1477
- var init_logger = __esm({
1478
- "../../packages/shared/tw/logger/logger.ts"() {
1479
- init_levels();
1480
- init_transports();
1481
- }
1482
- });
1483
-
1484
- // ../../packages/shared/tw/logger/progress.ts
1485
- var init_progress = __esm({
1486
- "../../packages/shared/tw/logger/progress.ts"() {
1487
- init_logger();
1488
- init_colors();
1489
- }
1490
- });
1491
-
1492
- // ../../packages/shared/tw/logger/index.ts
1493
- var init_logger2 = __esm({
1494
- "../../packages/shared/tw/logger/index.ts"() {
1495
- init_colors();
1496
- init_levels();
1497
- init_logger();
1498
- init_progress();
1499
- init_transports();
1500
- }
1501
- });
1502
-
1503
- // ../../packages/shared/tw/config/validate.ts
1504
- var init_validate = __esm({
1505
- "../../packages/shared/tw/config/validate.ts"() {
1506
- init_logger2();
1507
- }
1508
- });
1509
-
1510
- // ../../packages/shared/tw/config/loader.ts
1511
- var init_loader = __esm({
1512
- "../../packages/shared/tw/config/loader.ts"() {
1513
- init_defaults();
1514
- init_fs();
1515
- init_logger2();
1516
- init_validate();
1517
- }
1518
- });
1519
-
1520
- // ../../packages/shared/tw/config/index.ts
1521
- var init_config = __esm({
1522
- "../../packages/shared/tw/config/index.ts"() {
1523
- init_defaults();
1524
- init_loader();
1525
- init_validate();
1526
- }
1527
- });
1528
-
1529
1120
  // ../../packages/shared/tw/constants/aria.ts
1530
1121
  var init_aria = __esm({
1531
1122
  "../../packages/shared/tw/constants/aria.ts"() {
@@ -1877,195 +1468,781 @@ var init_html = __esm({
1877
1468
  }
1878
1469
  });
1879
1470
 
1880
- // ../../packages/shared/tw/constants/http.ts
1881
- var init_http = __esm({
1882
- "../../packages/shared/tw/constants/http.ts"() {
1471
+ // ../../packages/shared/tw/constants/http.ts
1472
+ var init_http = __esm({
1473
+ "../../packages/shared/tw/constants/http.ts"() {
1474
+ }
1475
+ });
1476
+
1477
+ // ../../packages/shared/tw/constants/index.ts
1478
+ var init_constants = __esm({
1479
+ "../../packages/shared/tw/constants/index.ts"() {
1480
+ init_aria();
1481
+ init_css();
1482
+ init_events();
1483
+ init_html();
1484
+ init_http();
1485
+ init_misc();
1486
+ }
1487
+ });
1488
+
1489
+ // ../../packages/shared/tw/crypto/index.ts
1490
+ var init_crypto = __esm({
1491
+ "../../packages/shared/tw/crypto/index.ts"() {
1492
+ }
1493
+ });
1494
+
1495
+ // ../../packages/shared/tw/env/environment.ts
1496
+ var init_environment = __esm({
1497
+ "../../packages/shared/tw/env/environment.ts"() {
1498
+ }
1499
+ });
1500
+
1501
+ // ../../packages/shared/tw/env/index.ts
1502
+ var init_env = __esm({
1503
+ "../../packages/shared/tw/env/index.ts"() {
1504
+ init_environment();
1505
+ }
1506
+ });
1507
+
1508
+ // ../../packages/shared/tw/hashing/etag.ts
1509
+ var init_etag = __esm({
1510
+ "../../packages/shared/tw/hashing/etag.ts"() {
1511
+ init_md5();
1512
+ init_sha();
1513
+ }
1514
+ });
1515
+
1516
+ // ../../packages/shared/tw/hashing/hmac.ts
1517
+ var init_hmac = __esm({
1518
+ "../../packages/shared/tw/hashing/hmac.ts"() {
1519
+ }
1520
+ });
1521
+
1522
+ // ../../packages/shared/tw/hashing/merkle.ts
1523
+ var init_merkle = __esm({
1524
+ "../../packages/shared/tw/hashing/merkle.ts"() {
1525
+ init_sha();
1526
+ }
1527
+ });
1528
+
1529
+ // ../../packages/shared/tw/hashing/uuid.ts
1530
+ var init_uuid = __esm({
1531
+ "../../packages/shared/tw/hashing/uuid.ts"() {
1532
+ }
1533
+ });
1534
+
1535
+ // ../../packages/shared/tw/hashing/index.ts
1536
+ var init_hashing = __esm({
1537
+ "../../packages/shared/tw/hashing/index.ts"() {
1538
+ init_crc32();
1539
+ init_etag();
1540
+ init_hmac();
1541
+ init_md5();
1542
+ init_merkle();
1543
+ init_sha();
1544
+ init_uuid();
1545
+ }
1546
+ });
1547
+
1548
+ // ../../packages/shared/tw/routing/constants.ts
1549
+ function getRouteFileType(baseName) {
1550
+ return SPECIAL_FILES[baseName] ?? null;
1551
+ }
1552
+ function isPrivateFolder(name) {
1553
+ return name.startsWith("_");
1554
+ }
1555
+ function extractGroupName(name) {
1556
+ const m = name.match(/^\(([^)]+)\)$/);
1557
+ if (!m) return null;
1558
+ if (m[1].startsWith(".")) return null;
1559
+ return m[1];
1560
+ }
1561
+ function parseInterceptingFolder(name) {
1562
+ const m = name.match(/^\((\.{1,3})([^)]+)\)$/);
1563
+ if (!m) return null;
1564
+ return { levels: m[1].length, target: m[2] };
1565
+ }
1566
+ function parseSegment(name) {
1567
+ const optCatchAll = name.match(/^\[\[\.\.\.([^\]]+)\]\]$/);
1568
+ if (optCatchAll) return { type: "optional-catch-all", name: optCatchAll[1] };
1569
+ const catchAll = name.match(/^\[\.\.\.([^\]]+)\]$/);
1570
+ if (catchAll) return { type: "catch-all", name: catchAll[1] };
1571
+ const dynamic = name.match(/^\[([^\]]+)\]$/);
1572
+ if (dynamic) return { type: "dynamic", name: dynamic[1] };
1573
+ return { type: "static", name };
1574
+ }
1575
+ function extractSlotName(name) {
1576
+ if (!name.startsWith("@")) return null;
1577
+ return name.slice(1);
1578
+ }
1579
+ var TW_EXTENSION, TWM_EXTENSION, ROUTE_EXTENSIONS, SPECIAL_FILES;
1580
+ var init_constants2 = __esm({
1581
+ "../../packages/shared/tw/routing/constants.ts"() {
1582
+ TW_EXTENSION = ".tw";
1583
+ TWM_EXTENSION = ".twm";
1584
+ ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([TW_EXTENSION, TWM_EXTENSION]);
1585
+ SPECIAL_FILES = {
1586
+ "page": "page",
1587
+ "index": "page",
1588
+ // Intercepting routes (docs/intercepting-routes.md): a `(.)page.tw`
1589
+ // beside a page renders INSTEAD of it on SPA navigation (X-TW-Navigate).
1590
+ "(.)page": "intercept-page",
1591
+ "layout": "layout",
1592
+ "template": "template",
1593
+ "loading": "loading",
1594
+ "error": "error",
1595
+ "not-found": "not-found",
1596
+ "global-error": "global-error",
1597
+ "default": "default",
1598
+ "head": "head",
1599
+ "route": "route",
1600
+ "middleware": "middleware"
1601
+ };
1602
+ }
1603
+ });
1604
+
1605
+ // ../../packages/shared/tw/routing/index.ts
1606
+ var init_routing = __esm({
1607
+ "../../packages/shared/tw/routing/index.ts"() {
1608
+ init_constants2();
1883
1609
  }
1884
1610
  });
1885
1611
 
1886
- // ../../packages/shared/tw/constants/index.ts
1887
- var init_constants = __esm({
1888
- "../../packages/shared/tw/constants/index.ts"() {
1889
- init_aria();
1890
- init_css();
1891
- init_events();
1892
- init_html();
1893
- init_http();
1894
- init_misc();
1612
+ // ../../packages/shared/tw/types/common.ts
1613
+ var init_common = __esm({
1614
+ "../../packages/shared/tw/types/common.ts"() {
1895
1615
  }
1896
1616
  });
1897
1617
 
1898
- // ../../packages/shared/tw/crypto/index.ts
1899
- var init_crypto = __esm({
1900
- "../../packages/shared/tw/crypto/index.ts"() {
1618
+ // ../../packages/shared/tw/types/errors.ts
1619
+ var init_errors = __esm({
1620
+ "../../packages/shared/tw/types/errors.ts"() {
1901
1621
  }
1902
1622
  });
1903
1623
 
1904
- // ../../packages/shared/tw/env/environment.ts
1905
- var init_environment = __esm({
1906
- "../../packages/shared/tw/env/environment.ts"() {
1624
+ // ../../packages/shared/tw/types/results.ts
1625
+ var init_results = __esm({
1626
+ "../../packages/shared/tw/types/results.ts"() {
1907
1627
  }
1908
1628
  });
1909
1629
 
1910
- // ../../packages/shared/tw/env/index.ts
1911
- var init_env = __esm({
1912
- "../../packages/shared/tw/env/index.ts"() {
1913
- init_environment();
1630
+ // ../../packages/shared/tw/types/index.ts
1631
+ var init_types = __esm({
1632
+ "../../packages/shared/tw/types/index.ts"() {
1633
+ init_common();
1634
+ init_errors();
1635
+ init_results();
1914
1636
  }
1915
1637
  });
1916
1638
 
1917
- // ../../packages/shared/tw/hashing/etag.ts
1918
- var init_etag = __esm({
1919
- "../../packages/shared/tw/hashing/etag.ts"() {
1920
- init_md5();
1639
+ // ../../packages/shared/tw/utils/string/transform.ts
1640
+ var init_transform = __esm({
1641
+ "../../packages/shared/tw/utils/string/transform.ts"() {
1642
+ }
1643
+ });
1644
+
1645
+ // ../../packages/shared/tw/index.ts
1646
+ var init_tw = __esm({
1647
+ "../../packages/shared/tw/index.ts"() {
1921
1648
  init_sha();
1649
+ init_utils();
1650
+ init_config();
1651
+ init_cache();
1652
+ init_constants();
1653
+ init_crypto();
1654
+ init_env();
1655
+ init_fs();
1656
+ init_hashing();
1657
+ init_logger2();
1658
+ init_routing();
1659
+ init_types();
1660
+ init_transform();
1922
1661
  }
1923
1662
  });
1924
1663
 
1925
- // ../../packages/shared/tw/hashing/hmac.ts
1926
- var init_hmac = __esm({
1927
- "../../packages/shared/tw/hashing/hmac.ts"() {
1664
+ // ../../packages/shared/tw/node-import.ts
1665
+ var node_import_exports = {};
1666
+ __export(node_import_exports, {
1667
+ twImportTs: () => twImportTs
1668
+ });
1669
+ async function twImportTs(tsPath) {
1670
+ if (!globalThis.__TW_NODE) {
1671
+ return import(tsPath);
1672
+ }
1673
+ const esbuildMain = globalThis.__TW_ESBUILD_MODULE;
1674
+ const esbuild = await (esbuildMain ? import(esbuildMain) : import("esbuild"));
1675
+ const safe = tsPath.replace(/[^a-zA-Z0-9]/g, "_").slice(-120);
1676
+ const cachePath = (0, import_node_path.join)((0, import_node_os.tmpdir)(), "tw-" + safe + ".mjs");
1677
+ await esbuild.build({
1678
+ entryPoints: [tsPath],
1679
+ bundle: true,
1680
+ format: "esm",
1681
+ platform: "node",
1682
+ target: "node18",
1683
+ outfile: cachePath,
1684
+ logLevel: "silent"
1685
+ });
1686
+ return import((0, import_node_url.pathToFileURL)(cachePath).href);
1687
+ }
1688
+ var import_node_path, import_node_os, import_node_url;
1689
+ var init_node_import = __esm({
1690
+ "../../packages/shared/tw/node-import.ts"() {
1691
+ import_node_path = require("node:path");
1692
+ import_node_os = require("node:os");
1693
+ import_node_url = require("node:url");
1694
+ }
1695
+ });
1696
+
1697
+ // ../../packages/image/tw/index.ts
1698
+ function optimizedImageUrl(src, opts = {}) {
1699
+ const p = new URLSearchParams();
1700
+ if (opts.width) p.set("w", String(opts.width));
1701
+ if (opts.height) p.set("h", String(opts.height));
1702
+ if (opts.quality) p.set("q", String(opts.quality));
1703
+ p.set("f", opts.format ?? "auto");
1704
+ const qs = p.toString();
1705
+ return `/_tw/img${src.startsWith("/") ? src : "/" + src}?${qs}`;
1706
+ }
1707
+ function imageAttributesFor(opts, config = DEFAULT_IMAGE_CONFIG) {
1708
+ const width = opts.width ?? 0;
1709
+ const quality = opts.quality ?? config.quality;
1710
+ const format = config.formats[0] ?? "webp";
1711
+ const isRemote = /^https?:\/\//i.test(opts.src);
1712
+ const src = isRemote ? `/_tw/img/_remote?src=${encodeURIComponent(opts.src)}&w=${opts.width ?? ""}&h=${opts.height ?? ""}&q=${quality}&f=auto` : optimizedImageUrl(opts.src, { width: opts.width, height: opts.height, quality });
1713
+ const widths = config.breakpoints.filter((bp) => width === 0 || bp <= width * 2);
1714
+ const srcset = widths.map(
1715
+ (bp) => isRemote ? `/_tw/img/_remote?src=${encodeURIComponent(opts.src)}&w=${bp}&q=${quality}&f=auto ${bp}w` : `${optimizedImageUrl(opts.src, { width: bp, quality })} ${bp}w`
1716
+ ).join(", ");
1717
+ const sizes = width > 0 ? `(max-width: ${width}px) 100vw, ${width}px` : "100vw";
1718
+ const loading = opts.priority ? "eager" : "lazy";
1719
+ return { src, srcset, sizes, width, height: opts.height ?? 0, loading, decoding: "async", format };
1720
+ }
1721
+ function createImageHandler(options) {
1722
+ const rootDir = (0, import_node_path2.resolve)(options.rootDir);
1723
+ const sourceDir = (0, import_node_path2.resolve)(options.sourceDir ?? (0, import_node_path2.join)(rootDir, "public"));
1724
+ const cacheDir = (0, import_node_path2.resolve)(options.cacheDir ?? (0, import_node_path2.join)(rootDir, ".tw", "img", "cache"));
1725
+ const config = { ...DEFAULT_IMAGE_CONFIG, ...options.config };
1726
+ let sharpPromise = null;
1727
+ let warnedNoSharp = false;
1728
+ async function loadSharp() {
1729
+ if (!sharpPromise) {
1730
+ sharpPromise = (async () => {
1731
+ let req;
1732
+ try {
1733
+ const { createRequire } = await import("node:module");
1734
+ req = createRequire((0, import_node_path2.join)(rootDir, "package.json"));
1735
+ } catch {
1736
+ }
1737
+ const candidates = [
1738
+ (0, import_node_path2.join)(rootDir, "node_modules", "sharp"),
1739
+ "sharp"
1740
+ ];
1741
+ for (const candidate of candidates) {
1742
+ if (typeof req === "function") {
1743
+ try {
1744
+ const m = req(candidate);
1745
+ const mod = m?.default ?? m;
1746
+ if (mod && typeof mod === "function") return mod;
1747
+ } catch {
1748
+ }
1749
+ }
1750
+ try {
1751
+ const href = candidate.startsWith("/") ? (0, import_node_url2.pathToFileURL)(candidate).href : candidate;
1752
+ const m = await import(href);
1753
+ const mod = m?.default ?? m;
1754
+ if (mod && typeof mod === "function") return mod;
1755
+ } catch {
1756
+ }
1757
+ }
1758
+ return null;
1759
+ })();
1760
+ }
1761
+ return sharpPromise;
1762
+ }
1763
+ function contentTypeFor(path) {
1764
+ return IMAGE_EXTENSIONS[(0, import_node_path2.extname)(path).toLowerCase()] ?? "application/octet-stream";
1765
+ }
1766
+ function negotiateFormat(requested, accept) {
1767
+ if (requested && requested !== "auto") {
1768
+ return SHARP_FORMATS[requested.toLowerCase()] ?? null;
1769
+ }
1770
+ const acceptLc = (accept ?? "").toLowerCase();
1771
+ for (const f of config.formats) {
1772
+ if (acceptLc.includes(`image/${f}`)) return f === "jpeg" ? "jpeg" : f;
1773
+ }
1774
+ return null;
1775
+ }
1776
+ function isRemoteSource(src) {
1777
+ return /^https?:\/\//i.test(src);
1778
+ }
1779
+ function remoteAllowed(url) {
1780
+ if (!config.remoteAllowHosts.includes(url.hostname)) return false;
1781
+ if (url.username || url.password) return false;
1782
+ return url.protocol === "https:" || url.protocol === "http:";
1783
+ }
1784
+ function isPrivateAddress(host) {
1785
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
1786
+ if (m) {
1787
+ const a = Number(m[1]), b = Number(m[2]);
1788
+ if (a === 0 || a === 10 || a === 127) return true;
1789
+ if (a === 169 && b === 254) return true;
1790
+ if (a === 172 && b >= 16 && b <= 31) return true;
1791
+ if (a === 192 && b === 168) return true;
1792
+ if (a >= 224) return true;
1793
+ return false;
1794
+ }
1795
+ const h = host.toLowerCase();
1796
+ if (h === "::" || h === "::1") return true;
1797
+ if (h.startsWith("fe80:")) return true;
1798
+ if (h.startsWith("fc") || h.startsWith("fd")) return true;
1799
+ if (h.startsWith("::ffff:")) return isPrivateAddress(h.slice(7));
1800
+ return false;
1801
+ }
1802
+ async function safeFetchRemote(src) {
1803
+ let url;
1804
+ try {
1805
+ url = new URL(src);
1806
+ } catch {
1807
+ return null;
1808
+ }
1809
+ for (let hop = 0; hop < 3; hop++) {
1810
+ if (!remoteAllowed(url)) return null;
1811
+ try {
1812
+ const dns = await import("node:dns");
1813
+ const addrs = await dns.promises.lookup(url.hostname, { all: true });
1814
+ if (!addrs.length) return null;
1815
+ for (const a of addrs) {
1816
+ if (isPrivateAddress(a.address)) return null;
1817
+ }
1818
+ } catch {
1819
+ return null;
1820
+ }
1821
+ let res;
1822
+ try {
1823
+ res = await fetch(url, { redirect: "manual" });
1824
+ } catch {
1825
+ return null;
1826
+ }
1827
+ if (res.status >= 300 && res.status < 400) {
1828
+ const loc = res.headers.get("location");
1829
+ if (!loc) return null;
1830
+ try {
1831
+ url = new URL(loc, url);
1832
+ } catch {
1833
+ return null;
1834
+ }
1835
+ continue;
1836
+ }
1837
+ if (!res.ok) return null;
1838
+ return Buffer.from(await res.arrayBuffer());
1839
+ }
1840
+ return null;
1841
+ }
1842
+ async function readSource(src) {
1843
+ if (isRemoteSource(src)) {
1844
+ const data = await safeFetchRemote(src);
1845
+ return data ? { data, mtime: 0 } : null;
1846
+ }
1847
+ let decoded;
1848
+ try {
1849
+ decoded = decodeURIComponent(src);
1850
+ } catch {
1851
+ return null;
1852
+ }
1853
+ const clean = (0, import_node_path2.normalize)(decoded).replace(/\\/g, "/");
1854
+ if (clean.includes("\0") || clean.includes("..")) return null;
1855
+ const file = (0, import_node_path2.resolve)(sourceDir, "." + (clean.startsWith("/") ? clean : "/" + clean));
1856
+ if (!file.startsWith(sourceDir + "/") && file !== sourceDir) return null;
1857
+ const dot = clean.split("/").pop() ?? "";
1858
+ if (dot.startsWith(".")) return null;
1859
+ if (!(0, import_node_fs.existsSync)(file)) return null;
1860
+ const st = (0, import_node_fs.statSync)(file);
1861
+ if (!st.isFile()) return null;
1862
+ return { data: (0, import_node_fs.readFileSync)(file), mtime: st.mtimeMs };
1863
+ }
1864
+ function cacheKey(src, w, h, q, f, mtime) {
1865
+ return (0, import_node_crypto2.createHash)("sha256").update(`${src}|${mtime}|${w}|${h}|${q}|${f}`).digest("hex").slice(0, 32);
1866
+ }
1867
+ async function processBuffer(data, w, h, q, format) {
1868
+ const sharp = await loadSharp();
1869
+ if (sharp && format) {
1870
+ try {
1871
+ let img = sharp(data);
1872
+ if (w || h) {
1873
+ img = img.resize(w || void 0, h || void 0, {
1874
+ fit: "inside",
1875
+ withoutEnlargement: true
1876
+ });
1877
+ }
1878
+ if (format !== "jpeg" && format !== "png") {
1879
+ img = img.toFormat(format, { quality: q });
1880
+ } else if (format === "jpeg") {
1881
+ img = img.jpeg({ quality: q });
1882
+ }
1883
+ const out = await img.toBuffer();
1884
+ return { data: out, contentType: `image/${format}`, viaSharp: true };
1885
+ } catch (err2) {
1886
+ console.warn(` [tw/image] processing failed (${format}, ${w}x${h}), serving original: ${err2?.message ?? err2}`);
1887
+ return { data, contentType: "", viaSharp: false };
1888
+ }
1889
+ }
1890
+ if (!warnedNoSharp) {
1891
+ warnedNoSharp = true;
1892
+ console.warn(" [tw/image] sharp not installed \u2014 serving original images (bun add sharp to enable resizing/WebP/AVIF)");
1893
+ }
1894
+ return { data, contentType: "", viaSharp: false };
1895
+ }
1896
+ async function handle(request) {
1897
+ const url = new URL(request.url);
1898
+ const pathname = url.pathname;
1899
+ if (!pathname.startsWith("/_tw/img/") && pathname !== "/_tw/img") return null;
1900
+ if (request.method !== "GET" && request.method !== "HEAD") {
1901
+ return new Response("Method Not Allowed", { status: 405 });
1902
+ }
1903
+ const q = url.searchParams;
1904
+ let src;
1905
+ if (pathname === "/_tw/img/_remote") {
1906
+ src = q.get("src") ?? "";
1907
+ if (!src) return new Response("Not Found", { status: 404 });
1908
+ } else {
1909
+ src = pathname.slice("/_tw/img".length) || "/";
1910
+ if (src === "/" || src === "") return new Response("Not Found", { status: 404 });
1911
+ }
1912
+ const w = Math.max(0, Math.min(1e4, parseInt(q.get("w") ?? "0", 10) || 0));
1913
+ const h = Math.max(0, Math.min(1e4, parseInt(q.get("h") ?? "0", 10) || 0));
1914
+ const quality = Math.max(1, Math.min(100, parseInt(q.get("q") ?? String(config.quality), 10) || config.quality));
1915
+ const format = negotiateFormat(q.get("f"), request.headers.get("accept") ?? "");
1916
+ const source = await readSource(src);
1917
+ if (!source) return new Response("Not Found", { status: 404 });
1918
+ if ((0, import_node_path2.extname)(src).toLowerCase() === ".svg") {
1919
+ return new Response(new Uint8Array(source.data), {
1920
+ status: 200,
1921
+ headers: {
1922
+ "Content-Type": "image/svg+xml",
1923
+ "Cache-Control": `public, max-age=${config.cacheMaxAge}`,
1924
+ "X-TW-Image": "svg-passthrough"
1925
+ }
1926
+ });
1927
+ }
1928
+ if (!w && !h && !format) {
1929
+ return new Response(new Uint8Array(source.data), {
1930
+ status: 200,
1931
+ headers: {
1932
+ "Content-Type": contentTypeFor(src),
1933
+ "Cache-Control": `public, max-age=${config.cacheMaxAge}`,
1934
+ "X-TW-Image": "original"
1935
+ }
1936
+ });
1937
+ }
1938
+ const key = cacheKey(src, w, h, quality, format ?? "orig", source.mtime);
1939
+ const cacheFile = (0, import_node_path2.join)(cacheDir, `${key}${format ? "." + format : ""}`);
1940
+ const etag = `"${key}"`;
1941
+ if (request.headers.get("if-none-match") === etag) {
1942
+ return new Response(null, { status: 304, headers: { ETag: etag } });
1943
+ }
1944
+ if ((0, import_node_fs.existsSync)(cacheFile)) {
1945
+ const cached = (0, import_node_fs.readFileSync)(cacheFile);
1946
+ return new Response(new Uint8Array(cached), {
1947
+ status: 200,
1948
+ headers: {
1949
+ "Content-Type": contentTypeFor(cacheFile),
1950
+ "Cache-Control": `public, max-age=${config.cacheMaxAge}`,
1951
+ ETag: etag,
1952
+ "X-TW-Image": "cache"
1953
+ }
1954
+ });
1955
+ }
1956
+ const result = await processBuffer(source.data, w, h, quality, format);
1957
+ if (result.viaSharp) {
1958
+ try {
1959
+ (0, import_node_fs.mkdirSync)(cacheDir, { recursive: true });
1960
+ (0, import_node_fs.writeFileSync)(cacheFile, result.data);
1961
+ } catch {
1962
+ }
1963
+ }
1964
+ return new Response(new Uint8Array(result.data), {
1965
+ status: 200,
1966
+ headers: {
1967
+ "Content-Type": result.contentType || contentTypeFor(src),
1968
+ "Cache-Control": `public, max-age=${config.cacheMaxAge}`,
1969
+ ETag: etag,
1970
+ "X-TW-Image": result.viaSharp ? "optimized" : "passthrough"
1971
+ }
1972
+ });
1973
+ }
1974
+ return { handle, sourceDir, cacheDir, config };
1975
+ }
1976
+ var import_node_crypto2, import_node_fs, import_node_path2, import_node_url2, DEFAULT_IMAGE_CONFIG, IMAGE_EXTENSIONS, SHARP_FORMATS;
1977
+ var init_tw2 = __esm({
1978
+ "../../packages/image/tw/index.ts"() {
1979
+ import_node_crypto2 = require("node:crypto");
1980
+ import_node_fs = require("node:fs");
1981
+ import_node_path2 = require("node:path");
1982
+ import_node_url2 = require("node:url");
1983
+ DEFAULT_IMAGE_CONFIG = {
1984
+ quality: 75,
1985
+ formats: ["avif", "webp"],
1986
+ breakpoints: [320, 640, 768, 1024, 1280, 1920],
1987
+ remoteAllowHosts: [],
1988
+ cacheMaxAge: 86400
1989
+ };
1990
+ IMAGE_EXTENSIONS = {
1991
+ ".jpg": "image/jpeg",
1992
+ ".jpeg": "image/jpeg",
1993
+ ".png": "image/png",
1994
+ ".webp": "image/webp",
1995
+ ".avif": "image/avif",
1996
+ ".gif": "image/gif",
1997
+ ".svg": "image/svg+xml",
1998
+ ".ico": "image/x-icon"
1999
+ };
2000
+ SHARP_FORMATS = {
2001
+ avif: "avif",
2002
+ webp: "webp",
2003
+ jpeg: "jpeg",
2004
+ jpg: "jpeg",
2005
+ png: "png"
2006
+ };
2007
+ }
2008
+ });
2009
+
2010
+ // ../../packages/server/tw/websocket-manager.ts
2011
+ var DEFAULT_MAX_ROOMS, DEFAULT_MAX_ROOM_SIZE, WebSocketManager;
2012
+ var init_websocket_manager = __esm({
2013
+ "../../packages/server/tw/websocket-manager.ts"() {
2014
+ DEFAULT_MAX_ROOMS = 1e3;
2015
+ DEFAULT_MAX_ROOM_SIZE = 1e4;
2016
+ WebSocketManager = class {
2017
+ connections = /* @__PURE__ */ new Map();
2018
+ meta = /* @__PURE__ */ new Map();
2019
+ rooms = /* @__PURE__ */ new Map();
2020
+ nextId = 1;
2021
+ maxRooms;
2022
+ maxRoomSize;
2023
+ constructor(options = {}) {
2024
+ this.maxRooms = options.maxRooms ?? DEFAULT_MAX_ROOMS;
2025
+ this.maxRoomSize = options.maxRoomSize ?? DEFAULT_MAX_ROOM_SIZE;
2026
+ }
2027
+ /** Register a socket on open; returns the assigned connection id. */
2028
+ add(connection, meta = {}) {
2029
+ let id = connection?.data?.id ?? connection?.id;
2030
+ if (!id) {
2031
+ id = "ws-" + process.pid + "-" + this.nextId++;
2032
+ }
2033
+ if (connection && typeof connection === "object") {
2034
+ if (!connection.data) connection.data = { id };
2035
+ else if (!connection.data.id) connection.data.id = id;
2036
+ if (!connection.id) connection.id = id;
2037
+ }
2038
+ this.connections.set(id, connection);
2039
+ this.meta.set(id, meta);
2040
+ return id;
2041
+ }
2042
+ /** Lookup by connection id. */
2043
+ get(id) {
2044
+ return this.connections.get(id);
2045
+ }
2046
+ /** Deregister on close; auto-leaves rooms (last member out kills the room). */
2047
+ remove(id) {
2048
+ for (const room of this.rooms.values()) {
2049
+ room.members.delete(id);
2050
+ if (room.members.size === 0) this.rooms.delete(room.name);
2051
+ }
2052
+ this.meta.delete(id);
2053
+ return this.connections.delete(id);
2054
+ }
2055
+ /** Serialize once and send to every socket regardless of room membership. */
2056
+ broadcast(payload) {
2057
+ const data = typeof payload === "string" ? payload : JSON.stringify(payload);
2058
+ for (const conn of this.connections.values()) {
2059
+ this.safeSend(conn, data);
2060
+ }
2061
+ }
2062
+ /** Send to one connection. */
2063
+ send(id, payload) {
2064
+ const conn = this.connections.get(id);
2065
+ if (!conn) return false;
2066
+ this.safeSend(conn, typeof payload === "string" ? payload : JSON.stringify(payload));
2067
+ return true;
2068
+ }
2069
+ /** All live entries. */
2070
+ getConnections() {
2071
+ return [...this.connections.keys()].map((id) => ({ id, meta: this.meta.get(id) ?? {} }));
2072
+ }
2073
+ /** Explicit pre-creation with metadata (rooms also auto-create on join). */
2074
+ createRoom(name, metadata = {}) {
2075
+ if (this.rooms.has(name)) return;
2076
+ if (this.rooms.size >= this.maxRooms) {
2077
+ throw new Error(`maxRooms reached (${this.maxRooms}): cannot create "${name}"`);
2078
+ }
2079
+ this.rooms.set(name, { name, metadata, members: /* @__PURE__ */ new Set() });
2080
+ }
2081
+ /** Join a room (auto-creates). A full room refuses joins. */
2082
+ joinRoom(connectionId, name) {
2083
+ if (!this.connections.has(connectionId)) return false;
2084
+ let room = this.rooms.get(name);
2085
+ if (!room) {
2086
+ if (this.rooms.size >= this.maxRooms) {
2087
+ throw new Error(`maxRooms reached (${this.maxRooms}): cannot create "${name}"`);
2088
+ }
2089
+ room = { name, metadata: {}, members: /* @__PURE__ */ new Set() };
2090
+ this.rooms.set(name, room);
2091
+ }
2092
+ if (room.members.size >= this.maxRoomSize) return false;
2093
+ room.members.add(connectionId);
2094
+ return true;
2095
+ }
2096
+ /** Leave a room; the room disappears when its last member leaves. */
2097
+ leaveRoom(connectionId, name) {
2098
+ const room = this.rooms.get(name);
2099
+ if (!room) return false;
2100
+ const had = room.members.delete(connectionId);
2101
+ if (room.members.size === 0) this.rooms.delete(name);
2102
+ return had;
2103
+ }
2104
+ /** Iterate only that room's members — Set membership guarantees exactly one copy. */
2105
+ broadcastToRoom(name, payload) {
2106
+ const room = this.rooms.get(name);
2107
+ if (!room) return 0;
2108
+ const data = typeof payload === "string" ? payload : JSON.stringify(payload);
2109
+ let sent = 0;
2110
+ for (const id of room.members) {
2111
+ const conn = this.connections.get(id);
2112
+ if (this.safeSend(conn, data)) sent++;
2113
+ }
2114
+ return sent;
2115
+ }
2116
+ /** Room + members. */
2117
+ getRoom(name) {
2118
+ const room = this.rooms.get(name);
2119
+ if (!room) return void 0;
2120
+ return { name: room.name, metadata: room.metadata, members: [...room.members] };
2121
+ }
2122
+ /** All rooms. */
2123
+ getRooms() {
2124
+ return [...this.rooms.values()].map((r) => ({ name: r.name, members: r.members.size }));
2125
+ }
2126
+ safeSend(conn, data) {
2127
+ try {
2128
+ if (!conn || typeof conn?.send !== "function") return false;
2129
+ if (typeof conn.readyState === "number" && conn.readyState !== 1) return false;
2130
+ conn.send(data);
2131
+ return true;
2132
+ } catch {
2133
+ return false;
2134
+ }
2135
+ }
2136
+ };
1928
2137
  }
1929
2138
  });
1930
2139
 
1931
- // ../../packages/shared/tw/hashing/merkle.ts
1932
- var init_merkle = __esm({
1933
- "../../packages/shared/tw/hashing/merkle.ts"() {
1934
- init_sha();
2140
+ // ../../packages/server/tw/routing/layout-chain.ts
2141
+ function resolveLayoutChain(matchResult) {
2142
+ const { node, chain, params } = matchResult;
2143
+ const layouts = [];
2144
+ const templates = [];
2145
+ for (const nodeInChain of chain) {
2146
+ const layoutFile = nodeInChain.files.find((f) => f.type === "layout");
2147
+ if (layoutFile) layouts.push(layoutFile);
2148
+ const templateFile = nodeInChain.files.find((f) => f.type === "template");
2149
+ if (templateFile) templates.push(templateFile);
1935
2150
  }
1936
- });
1937
-
1938
- // ../../packages/shared/tw/hashing/uuid.ts
1939
- var init_uuid = __esm({
1940
- "../../packages/shared/tw/hashing/uuid.ts"() {
2151
+ let loading;
2152
+ let error;
2153
+ let notFound2;
2154
+ let head;
2155
+ for (let i = chain.length - 1; i >= 0; i--) {
2156
+ if (!loading) {
2157
+ loading = chain[i].files.find((f) => f.type === "loading");
2158
+ }
2159
+ if (!error) {
2160
+ error = chain[i].files.find((f) => f.type === "error");
2161
+ }
2162
+ if (!notFound2) {
2163
+ notFound2 = chain[i].files.find((f) => f.type === "not-found");
2164
+ }
2165
+ if (!head) {
2166
+ head = chain[i].files.find((f) => f.type === "head");
2167
+ }
1941
2168
  }
1942
- });
1943
-
1944
- // ../../packages/shared/tw/hashing/index.ts
1945
- var init_hashing = __esm({
1946
- "../../packages/shared/tw/hashing/index.ts"() {
1947
- init_crc32();
1948
- init_etag();
1949
- init_hmac();
1950
- init_md5();
1951
- init_merkle();
1952
- init_sha();
1953
- init_uuid();
2169
+ const page = node.files.find((f) => f.type === "page");
2170
+ let renderMode = "ssr";
2171
+ if (page) {
2172
+ renderMode = extractRenderMode(page);
1954
2173
  }
1955
- });
1956
-
1957
- // ../../packages/shared/tw/routing/constants.ts
1958
- function getRouteFileType(baseName) {
1959
- return SPECIAL_FILES[baseName] ?? null;
1960
- }
1961
- function isPrivateFolder(name) {
1962
- return name.startsWith("_");
1963
- }
1964
- function extractGroupName(name) {
1965
- const m = name.match(/^\(([^)]+)\)$/);
1966
- if (!m) return null;
1967
- if (m[1].startsWith(".")) return null;
1968
- return m[1];
2174
+ return {
2175
+ node,
2176
+ chain,
2177
+ params,
2178
+ layouts,
2179
+ templates,
2180
+ loading,
2181
+ error,
2182
+ notFound: notFound2,
2183
+ head,
2184
+ page,
2185
+ middleware: [],
2186
+ renderMode
2187
+ };
1969
2188
  }
1970
- function parseInterceptingFolder(name) {
1971
- const m = name.match(/^\((\.{1,3})([^)]+)\)$/);
1972
- if (!m) return null;
1973
- return { levels: m[1].length, target: m[2] };
2189
+ function findGlobalError(rootNode) {
2190
+ return rootNode.files.find((f) => f.type === "global-error");
1974
2191
  }
1975
- function parseSegment(name) {
1976
- const optCatchAll = name.match(/^\[\[\.\.\.([^\]]+)\]\]$/);
1977
- if (optCatchAll) return { type: "optional-catch-all", name: optCatchAll[1] };
1978
- const catchAll = name.match(/^\[\.\.\.([^\]]+)\]$/);
1979
- if (catchAll) return { type: "catch-all", name: catchAll[1] };
1980
- const dynamic = name.match(/^\[([^\]]+)\]$/);
1981
- if (dynamic) return { type: "dynamic", name: dynamic[1] };
1982
- return { type: "static", name };
2192
+ function findRootError(rootNode) {
2193
+ return (rootNode.files || []).find((f) => f.type === "error");
1983
2194
  }
1984
- function extractSlotName(name) {
1985
- if (!name.startsWith("@")) return null;
1986
- return name.slice(1);
2195
+ function findRootNotFound(rootNode) {
2196
+ return rootNode.files.find((f) => f.type === "not-found");
1987
2197
  }
1988
- var TW_EXTENSION, TWM_EXTENSION, ROUTE_EXTENSIONS, SPECIAL_FILES;
1989
- var init_constants2 = __esm({
1990
- "../../packages/shared/tw/routing/constants.ts"() {
1991
- TW_EXTENSION = ".tw";
1992
- TWM_EXTENSION = ".twm";
1993
- ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([TW_EXTENSION, TWM_EXTENSION]);
1994
- SPECIAL_FILES = {
1995
- "page": "page",
1996
- "index": "page",
1997
- // Intercepting routes (docs/intercepting-routes.md): a `(.)page.tw`
1998
- // beside a page renders INSTEAD of it on SPA navigation (X-TW-Navigate).
1999
- "(.)page": "intercept-page",
2000
- "layout": "layout",
2001
- "template": "template",
2002
- "loading": "loading",
2003
- "error": "error",
2004
- "not-found": "not-found",
2005
- "global-error": "global-error",
2006
- "default": "default",
2007
- "head": "head",
2008
- "route": "route",
2009
- "middleware": "middleware"
2010
- };
2011
- }
2012
- });
2013
-
2014
- // ../../packages/shared/tw/routing/index.ts
2015
- var init_routing = __esm({
2016
- "../../packages/shared/tw/routing/index.ts"() {
2017
- init_constants2();
2018
- }
2019
- });
2020
-
2021
- // ../../packages/shared/tw/types/common.ts
2022
- var init_common = __esm({
2023
- "../../packages/shared/tw/types/common.ts"() {
2024
- }
2025
- });
2026
-
2027
- // ../../packages/shared/tw/types/errors.ts
2028
- var init_errors = __esm({
2029
- "../../packages/shared/tw/types/errors.ts"() {
2030
- }
2031
- });
2032
-
2033
- // ../../packages/shared/tw/types/results.ts
2034
- var init_results = __esm({
2035
- "../../packages/shared/tw/types/results.ts"() {
2198
+ function collectParallelDefaults(node) {
2199
+ const defaults = /* @__PURE__ */ new Map();
2200
+ for (const child of node.children) {
2201
+ if (child.kind === "parallel" && child.slotName) {
2202
+ const defaultFile = child.files.find((f) => f.type === "default");
2203
+ if (defaultFile) {
2204
+ defaults.set(child.slotName, defaultFile);
2205
+ }
2206
+ }
2036
2207
  }
2037
- });
2038
-
2039
- // ../../packages/shared/tw/types/index.ts
2040
- var init_types = __esm({
2041
- "../../packages/shared/tw/types/index.ts"() {
2042
- init_common();
2043
- init_errors();
2044
- init_results();
2208
+ return defaults;
2209
+ }
2210
+ function collectParallelPages(node, pathParts, index) {
2211
+ const pages = /* @__PURE__ */ new Map();
2212
+ for (const child of node.children) {
2213
+ if (child.kind === "parallel" && child.slotName) {
2214
+ const pageFile = child.files.find((f) => f.type === "page");
2215
+ if (pageFile) {
2216
+ pages.set(child.slotName, pageFile);
2217
+ }
2218
+ if (index < pathParts.length) {
2219
+ for (const grandchild of child.children) {
2220
+ if (grandchild.kind === "static" && grandchild.segment === pathParts[index]) {
2221
+ const deepPage = grandchild.files.find((f) => f.type === "page");
2222
+ if (deepPage) {
2223
+ pages.set(child.slotName, deepPage);
2224
+ }
2225
+ }
2226
+ }
2227
+ }
2228
+ }
2045
2229
  }
2046
- });
2047
-
2048
- // ../../packages/shared/tw/utils/string/transform.ts
2049
- var init_transform = __esm({
2050
- "../../packages/shared/tw/utils/string/transform.ts"() {
2230
+ return pages;
2231
+ }
2232
+ function extractRenderMode(page) {
2233
+ try {
2234
+ const fs = require("node:fs");
2235
+ const source = fs.readFileSync(page.absolutePath, "utf-8");
2236
+ const m = source.match(/render\s+(static|ssr|island|edge)/);
2237
+ if (m) {
2238
+ return m[1];
2239
+ }
2240
+ } catch {
2051
2241
  }
2052
- });
2053
-
2054
- // ../../packages/shared/tw/index.ts
2055
- var init_tw2 = __esm({
2056
- "../../packages/shared/tw/index.ts"() {
2057
- init_sha();
2058
- init_utils();
2059
- init_config();
2060
- init_constants();
2061
- init_crypto();
2062
- init_env();
2063
- init_fs();
2064
- init_hashing();
2065
- init_logger2();
2066
- init_routing();
2067
- init_types();
2068
- init_transform();
2242
+ return "ssr";
2243
+ }
2244
+ var init_layout_chain = __esm({
2245
+ "../../packages/server/tw/routing/layout-chain.ts"() {
2069
2246
  }
2070
2247
  });
2071
2248
 
@@ -2416,7 +2593,7 @@ function readNestedBlocks(source, start, end) {
2416
2593
  }
2417
2594
  var init_blocks = __esm({
2418
2595
  "../../packages/compiler/tw/lexer/blocks.ts"() {
2419
- init_tw2();
2596
+ init_tw();
2420
2597
  }
2421
2598
  });
2422
2599
 
@@ -7750,6 +7927,12 @@ function parsePageDirective(cursor, token) {
7750
7927
  if (!optToken) break;
7751
7928
  const optName = optToken.value;
7752
7929
  cursor.skipWhitespace();
7930
+ if (optName === "cache" && cursor.peek() && cursor.peek().type === "LBRACE") {
7931
+ cursor.advance();
7932
+ options.cache = parseCacheBlockOptions(cursor, token);
7933
+ cursor.skipWhitespace();
7934
+ continue;
7935
+ }
7753
7936
  const nextToken = cursor.peek();
7754
7937
  if (nextToken && (nextToken.type === "STRING" || nextToken.type === "NUMBER" || nextToken.type === "IDENT" || nextToken.type === "KEYWORD")) {
7755
7938
  cursor.advance();
@@ -7787,6 +7970,42 @@ function parsePageDirective(cursor, token) {
7787
7970
  col: token.pos.col
7788
7971
  };
7789
7972
  }
7973
+ function parseCacheBlockOptions(cursor, pageToken) {
7974
+ const out = {};
7975
+ cursor.skipWhitespace();
7976
+ while (cursor.peek() && cursor.peek().type !== "RBRACE" && cursor.peek().type !== "EOF") {
7977
+ const keyToken = cursor.advance();
7978
+ if (!keyToken) break;
7979
+ const key = keyToken.value;
7980
+ cursor.skipWhitespace();
7981
+ cursor.match("EQUALS") || cursor.match("COLON");
7982
+ cursor.skipWhitespace();
7983
+ const valToken = cursor.peek();
7984
+ if (valToken && (valToken.type === "STRING" || valToken.type === "NUMBER")) {
7985
+ cursor.advance();
7986
+ out[key] = valToken.type === "NUMBER" ? parseFloat(valToken.value) : valToken.value;
7987
+ } else if (valToken && (valToken.type === "IDENT" || valToken.type === "KEYWORD")) {
7988
+ cursor.advance();
7989
+ out[key] = valToken.value;
7990
+ }
7991
+ cursor.skipWhitespace();
7992
+ cursor.match("COMMA");
7993
+ cursor.skipWhitespace();
7994
+ }
7995
+ cursor.consumeIf("RBRACE");
7996
+ if (out.revalidate == null && out.life == null) {
7997
+ cursor.errors.add({
7998
+ type: "error",
7999
+ message: 'cache { } requires a `revalidate N` window or a `life "profile"` (docs/cache-tags.md)',
8000
+ line: pageToken.pos.line,
8001
+ col: pageToken.pos.col,
8002
+ code: "TW090",
8003
+ severity: "error",
8004
+ category: "syntax"
8005
+ });
8006
+ }
8007
+ return out;
8008
+ }
7790
8009
  function parseHeadDirective(cursor, token, parseBody) {
7791
8010
  let title;
7792
8011
  const meta = [];
@@ -8540,7 +8759,7 @@ function parseBlockBody(cursor, parseBody) {
8540
8759
  var DIRECTIVE_KEYWORDS, SIGNAL_FUNCTIONS;
8541
8760
  var init_directives = __esm({
8542
8761
  "../../packages/compiler/tw/parser/directives/index.ts"() {
8543
- init_tw2();
8762
+ init_tw();
8544
8763
  DIRECTIVE_KEYWORDS = /* @__PURE__ */ new Set([
8545
8764
  "page",
8546
8765
  "head",
@@ -9861,7 +10080,7 @@ var init_statements = __esm({
9861
10080
  init_nodes();
9862
10081
  init_recovery();
9863
10082
  init_directives();
9864
- init_tw2();
10083
+ init_tw();
9865
10084
  CLASS_BOUNDARY_BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
9866
10085
  "disabled",
9867
10086
  "required",
@@ -11736,7 +11955,7 @@ var init_parser = __esm({
11736
11955
  init_recovery();
11737
11956
  init_statements();
11738
11957
  init_directives();
11739
- init_tw2();
11958
+ init_tw();
11740
11959
  init_parser_recovery();
11741
11960
  init_precedence_table();
11742
11961
  init_props();
@@ -17665,7 +17884,7 @@ var init_types4 = __esm({
17665
17884
  var TW_VERSION, TW_GENERATOR_META;
17666
17885
  var init_version = __esm({
17667
17886
  "../../packages/compiler/tw/codegen/version.ts"() {
17668
- TW_VERSION = "1.0.5";
17887
+ TW_VERSION = "1.0.6";
17669
17888
  TW_GENERATOR_META = `<meta name="generator" content="TW Framework ${TW_VERSION}">`;
17670
17889
  }
17671
17890
  });
@@ -17831,7 +18050,7 @@ function generateBuiltinTag(specifier, props, textContent) {
17831
18050
  var BUILTIN_COMPONENT_SPECIFIERS, imageConfig;
17832
18051
  var init_builtin_components = __esm({
17833
18052
  "../../packages/compiler/tw/codegen/builtin-components.ts"() {
17834
- init_tw();
18053
+ init_tw2();
17835
18054
  init_tw3();
17836
18055
  BUILTIN_COMPONENT_SPECIFIERS = ["@tw/optImage", "@tw/RouterLink"];
17837
18056
  imageConfig = DEFAULT_IMAGE_CONFIG;
@@ -23871,7 +24090,12 @@ var init_codes = __esm({
23871
24090
  TW086: { message: "Deprecated state declaration syntax", severity: "warning", category: "deprecation" },
23872
24091
  TW087: { message: "Deprecated component lifecycle hook", severity: "warning", category: "deprecation" },
23873
24092
  TW088: { message: "Deprecated import path", severity: "warning", category: "deprecation" },
23874
- TW089: { message: "Deprecated render mode -- use 'interactive'", severity: "warning", category: "deprecation" }
24093
+ TW089: { message: "Deprecated render mode -- use 'interactive'", severity: "warning", category: "deprecation" },
24094
+ // Cache (docs/cache-tags.md)
24095
+ TW090: { message: "cache { } requires a revalidate window or a life profile", severity: "error", category: "syntax" },
24096
+ TW091: { message: "fn cached handler is impure (request.cookies/headers/body or setSignal)", severity: "error", category: "semantic" },
24097
+ TW092: { message: "Unknown cache profile", severity: "error", category: "semantic" },
24098
+ TW093: { message: "Non-deterministic call in a cached handler (value freezes into the cache entry)", severity: "warning", category: "best-practice" }
23875
24099
  };
23876
24100
  }
23877
24101
  });
@@ -24970,6 +25194,7 @@ var init_rules = __esm({
24970
25194
  "row",
24971
25195
  "rowgroup",
24972
25196
  "rowheader",
25197
+ "table",
24973
25198
  "scrollbar",
24974
25199
  "search",
24975
25200
  "searchbox",
@@ -25001,6 +25226,10 @@ var init_rules = __esm({
25001
25226
  "aria-atomic",
25002
25227
  "aria-autocomplete",
25003
25228
  "aria-busy",
25229
+ "aria-braillelabel",
25230
+ "aria-brailleroledescription",
25231
+ "aria-description",
25232
+ "aria-details",
25004
25233
  "aria-checked",
25005
25234
  "aria-colcount",
25006
25235
  "aria-colindex",
@@ -28559,7 +28788,7 @@ var init_disk = __esm({
28559
28788
  "../../packages/compiler/tw/cache/disk.ts"() {
28560
28789
  import_node_fs3 = require("node:fs");
28561
28790
  import_node_path4 = require("node:path");
28562
- init_tw2();
28791
+ init_tw();
28563
28792
  DiskCache = class {
28564
28793
  constructor(cacheDir) {
28565
28794
  this.cacheDir = cacheDir;
@@ -28627,7 +28856,7 @@ var init_disk = __esm({
28627
28856
 
28628
28857
  // ../../packages/compiler/tw/cache/cache.ts
28629
28858
  var UnifiedCache;
28630
- var init_cache = __esm({
28859
+ var init_cache2 = __esm({
28631
28860
  "../../packages/compiler/tw/cache/cache.ts"() {
28632
28861
  init_memory();
28633
28862
  init_disk();
@@ -28692,14 +28921,14 @@ function makeConfigKey(config) {
28692
28921
  }
28693
28922
  var init_keys = __esm({
28694
28923
  "../../packages/compiler/tw/cache/keys.ts"() {
28695
- init_tw2();
28924
+ init_tw();
28696
28925
  }
28697
28926
  });
28698
28927
 
28699
28928
  // ../../packages/compiler/tw/cache/index.ts
28700
- var init_cache2 = __esm({
28929
+ var init_cache3 = __esm({
28701
28930
  "../../packages/compiler/tw/cache/index.ts"() {
28702
- init_cache();
28931
+ init_cache2();
28703
28932
  init_disk();
28704
28933
  init_keys();
28705
28934
  init_memory();
@@ -29074,7 +29303,7 @@ function removeFile(graph, file) {
29074
29303
  }
29075
29304
  var init_incremental = __esm({
29076
29305
  "../../packages/compiler/tw/incremental/index.ts"() {
29077
- init_tw2();
29306
+ init_tw();
29078
29307
  }
29079
29308
  });
29080
29309
 
@@ -34530,7 +34759,7 @@ var init_tw4 = __esm({
34530
34759
  init_server_components();
34531
34760
  init_suspense();
34532
34761
  init_ast();
34533
- init_cache2();
34762
+ init_cache3();
34534
34763
  init_codegen();
34535
34764
  init_codegen();
34536
34765
  init_diagnostics();
@@ -34900,7 +35129,7 @@ var init_scanner = __esm({
34900
35129
  "../../packages/server/tw/routing/scanner.ts"() {
34901
35130
  import_node_fs5 = require("node:fs");
34902
35131
  import_node_path6 = require("node:path");
34903
- init_tw2();
35132
+ init_tw();
34904
35133
  }
34905
35134
  });
34906
35135
 
@@ -34962,16 +35191,26 @@ var init_helpers = __esm({
34962
35191
  });
34963
35192
 
34964
35193
  // ../../packages/server/tw/routing/render-pipeline.ts
35194
+ function getCacheProfiles(rootDir) {
35195
+ if (__profilesCache && __profilesCache.rootDir === rootDir) return __profilesCache.profiles;
35196
+ let profiles = {};
35197
+ try {
35198
+ profiles = readCacheProfilesSync(rootDir);
35199
+ } catch {
35200
+ }
35201
+ __profilesCache = { rootDir, profiles };
35202
+ return profiles;
35203
+ }
34965
35204
  function createRenderPipeline(opts) {
34966
35205
  return new RenderPipeline(opts);
34967
35206
  }
34968
- var import_node_fs6, import_node_path7, RenderPipeline;
35207
+ var import_node_fs6, import_node_path7, __profilesCache, RenderPipeline;
34969
35208
  var init_render_pipeline = __esm({
34970
35209
  "../../packages/server/tw/routing/render-pipeline.ts"() {
34971
35210
  import_node_fs6 = require("node:fs");
34972
35211
  import_node_path7 = require("node:path");
34973
35212
  init_tw4();
34974
- init_tw2();
35213
+ init_tw();
34975
35214
  init_twm_loader();
34976
35215
  init_scanner();
34977
35216
  init_layout_chain();
@@ -34979,6 +35218,7 @@ var init_render_pipeline = __esm({
34979
35218
  init_tw6();
34980
35219
  init_twm_loader();
34981
35220
  init_tw6();
35221
+ __profilesCache = null;
34982
35222
  RenderPipeline = class {
34983
35223
  rootDir;
34984
35224
  homeDir;
@@ -34989,6 +35229,17 @@ var init_render_pipeline = __esm({
34989
35229
  dev;
34990
35230
  routeTree = null;
34991
35231
  compiledCache = /* @__PURE__ */ new Map();
35232
+ /**
35233
+ * Render cache (docs/cache-tags.md). Entry fields:
35234
+ * expiresAt -- v1.0.5 compat: fresh-until timestamp (same as freshUntil)
35235
+ * freshUntil -- age < freshUntil -> HIT (zero work)
35236
+ * expireAt -- freshUntil <= age < expireAt -> STALE + background
35237
+ * refresh; age >= expireAt -> dropped (blocking MISS)
35238
+ * cache -- resolved cache config (null for plain TTL entries); its
35239
+ * `tag` is the single source of truth for revalidateTag() -- the tag
35240
+ * "index" is derived by scanning entries (<= 100), which is the
35241
+ * documented lazy rebuild (design 6.3).
35242
+ */
34992
35243
  renderCache = /* @__PURE__ */ new Map();
34993
35244
  constructor(opts) {
34994
35245
  this.rootDir = opts.rootDir;
@@ -35035,15 +35286,18 @@ var init_render_pipeline = __esm({
35035
35286
  const cacheKey = sha256(`${pathname}:${JSON.stringify(stateVars ?? {})}:${this.navRequest ? "nav" : ""}`);
35036
35287
  if (this.enableCache) {
35037
35288
  const cached = this.renderCache.get(cacheKey);
35038
- if (cached && Date.now() < cached.expiresAt) {
35289
+ const now = Date.now();
35290
+ const freshUntil = cached ? cached.freshUntil ?? cached.expiresAt : 0;
35291
+ const expireAt = cached ? cached.expireAt ?? freshUntil : 0;
35292
+ if (cached && now < freshUntil) {
35039
35293
  return {
35040
35294
  ...cached.result,
35041
35295
  fromCache: true,
35042
35296
  durationMs: 0,
35043
- headers: { ...cached.result.headers, "x-tw-cache": "HIT" }
35297
+ headers: this.stampCacheHeaders(cached.result.headers, cached, now, "HIT")
35044
35298
  };
35045
35299
  }
35046
- if (cached && cached.revalidate && !this.pendingRefresh.has(cacheKey)) {
35300
+ if (cached && now < expireAt && cached.swr && !this.pendingRefresh.has(cacheKey)) {
35047
35301
  this.pendingRefresh.add(cacheKey);
35048
35302
  void Promise.resolve().then(() => {
35049
35303
  try {
@@ -35058,9 +35312,10 @@ var init_render_pipeline = __esm({
35058
35312
  fromCache: true,
35059
35313
  stale: true,
35060
35314
  durationMs: 0,
35061
- headers: { ...cached.result.headers, "x-tw-cache": "STALE" }
35315
+ headers: this.stampCacheHeaders(cached.result.headers, cached, now, "STALE")
35062
35316
  };
35063
35317
  }
35318
+ if (cached && now >= expireAt) this.renderCache.delete(cacheKey);
35064
35319
  }
35065
35320
  const tree = this.getRouteTree();
35066
35321
  if (!tree) {
@@ -35229,20 +35484,26 @@ ${bodyHTML}`;
35229
35484
  durationMs: performance.now() - startTime,
35230
35485
  renderMode: this.extractRenderMode(ctx.page)
35231
35486
  };
35232
- const pageRevalidate = this.extractRevalidate(ctx.page);
35487
+ const pageCache = this.extractCacheConfig(ctx.page);
35233
35488
  if (this.enableCache) {
35234
35489
  if (this.renderCache.size >= 100) {
35235
35490
  const oldest = this.renderCache.keys().next().value;
35236
35491
  if (oldest) this.renderCache.delete(oldest);
35237
35492
  }
35493
+ const freshMs = pageCache ? pageCache.revalidate * 1e3 : this.cacheTTL;
35494
+ const expireMs = pageCache ? pageCache.expire * 1e3 : this.cacheTTL;
35238
35495
  this.renderCache.set(cacheKey, {
35239
35496
  result,
35240
- expiresAt: Date.now() + (pageRevalidate ? pageRevalidate * 1e3 : this.cacheTTL),
35241
- revalidate: pageRevalidate,
35497
+ expiresAt: Date.now() + freshMs,
35498
+ freshUntil: Date.now() + freshMs,
35499
+ expireAt: Date.now() + expireMs,
35500
+ swr: !!pageCache,
35501
+ cache: pageCache,
35502
+ createdAt: Date.now(),
35242
35503
  pathname
35243
35504
  });
35244
35505
  }
35245
- return { ...result, headers: { ...result.headers, "x-tw-cache": "MISS" } };
35506
+ return { ...result, headers: this.stampCacheHeaders(result.headers, this.renderCache.get(cacheKey), Date.now(), "MISS") };
35246
35507
  } catch (err2) {
35247
35508
  const globalError = findGlobalError(tree);
35248
35509
  if (globalError) {
@@ -35413,6 +35674,45 @@ ${bodyHTML}`;
35413
35674
  return "static";
35414
35675
  }
35415
35676
  }
35677
+ /**
35678
+ * Cache tags (docs/cache-tags.md): expire every render-cache entry
35679
+ * whose `cache { tag "..." }` family matches. The next request for those
35680
+ * routes renders fresh (MISS). Entries are the single source of truth;
35681
+ * scanning them is the derived tag index (rebuilt by construction).
35682
+ * Returns the number of dropped entries.
35683
+ */
35684
+ revalidateTag(tag) {
35685
+ let dropped = 0;
35686
+ for (const [key, entry] of this.renderCache) {
35687
+ const c = entry.cache;
35688
+ if (c && c.tag === tag) {
35689
+ this.renderCache.delete(key);
35690
+ dropped++;
35691
+ }
35692
+ }
35693
+ return dropped;
35694
+ }
35695
+ /**
35696
+ * Cache headers on every cacheable response (docs/cache-tags.md):
35697
+ * x-tw-cache: HIT | STALE | MISS (existing contract, unchanged)
35698
+ * x-tw-cache-age: seconds since the entry was rendered (observation)
35699
+ * Cache-Control: public, max-age=<stale>, stale-while-revalidate=...
35700
+ * only when `stale > 0` -- the client-side hint. The server
35701
+ * render-cache never reads `stale`.
35702
+ */
35703
+ stampCacheHeaders(headers, entry, now, verdict) {
35704
+ const out = { ...headers ?? {}, "x-tw-cache": verdict };
35705
+ const c = entry?.cache;
35706
+ if (c) {
35707
+ const ageSec = entry?.createdAt ? Math.max(0, Math.floor((now - entry.createdAt) / 1e3)) : 0;
35708
+ out["x-tw-cache-age"] = String(ageSec);
35709
+ if (c.stale > 0) {
35710
+ const swr = Number.isFinite(c.expire) ? ", stale-while-revalidate=" + Math.max(0, c.expire - c.stale) : "";
35711
+ out["Cache-Control"] = "public, max-age=" + c.stale + swr;
35712
+ }
35713
+ }
35714
+ return out;
35715
+ }
35416
35716
  extractRevalidate(file) {
35417
35717
  try {
35418
35718
  const source = (0, import_node_fs6.readFileSync)(file.absolutePath, "utf-8");
@@ -35426,6 +35726,34 @@ ${bodyHTML}`;
35426
35726
  return null;
35427
35727
  }
35428
35728
  }
35729
+ /**
35730
+ * Cache config (docs/cache-tags.md): resolve a page's `cache { }`
35731
+ * directive against tw.config.ts cache.profiles, falling back to the
35732
+ * legacy `revalidate N` ISR form. The legacy form desugars to
35733
+ * { revalidate: N, stale: 0, expire: Infinity } -- the exact v1.0.5
35734
+ * behavior (fresh for N seconds, then stale-while-revalidate forever).
35735
+ */
35736
+ extractCacheConfig(file) {
35737
+ try {
35738
+ const source = (0, import_node_fs6.readFileSync)(file.absolutePath, "utf-8");
35739
+ const meta = extractCacheDirective(source);
35740
+ if (meta) {
35741
+ try {
35742
+ return resolveCache(meta, getCacheProfiles(this.rootDir));
35743
+ } catch (e) {
35744
+ console.error("[tw] " + e.message + " (" + file.absolutePath + ")");
35745
+ return null;
35746
+ }
35747
+ }
35748
+ const legacy = this.extractRevalidate(file);
35749
+ if (legacy != null) {
35750
+ return { revalidate: legacy, stale: 0, expire: Infinity };
35751
+ }
35752
+ return null;
35753
+ } catch {
35754
+ return null;
35755
+ }
35756
+ }
35429
35757
  /**
35430
35758
  * Metadata API (docs/metadata.md): description / keywords / og_title /
35431
35759
  * og_description / og_image in the page frontmatter become <meta> tags --
@@ -37161,8 +37489,8 @@ var init_static = __esm({
37161
37489
  "../../packages/server/tw/static/index.ts"() {
37162
37490
  import_node_fs8 = require("node:fs");
37163
37491
  import_node_path9 = require("node:path");
37164
- init_tw2();
37165
- init_tw2();
37492
+ init_tw();
37493
+ init_tw();
37166
37494
  StaticHandler = class {
37167
37495
  root;
37168
37496
  maxAge;
@@ -46119,7 +46447,7 @@ var import_node_fs9, import_node_crypto5, import_node_path10, import_node_fs10,
46119
46447
  var init_ssr = __esm({
46120
46448
  "../../packages/server/tw/ssr/index.ts"() {
46121
46449
  import_node_fs9 = require("node:fs");
46122
- init_tw2();
46450
+ init_tw();
46123
46451
  import_node_crypto5 = require("node:crypto");
46124
46452
  import_node_path10 = require("node:path");
46125
46453
  import_node_fs10 = require("node:fs");
@@ -47136,7 +47464,7 @@ function applyRewrites(rewrites, pathname) {
47136
47464
  var import_node_path11, import_node_fs11, TWServer;
47137
47465
  var init_tw6 = __esm({
47138
47466
  "../../packages/server/tw/index.ts"() {
47139
- init_tw();
47467
+ init_tw2();
47140
47468
  init_websocket_manager();
47141
47469
  init_routing2();
47142
47470
  import_node_path11 = require("node:path");
@@ -47162,7 +47490,9 @@ var init_tw6 = __esm({
47162
47490
  router;
47163
47491
  pipeline;
47164
47492
  staticHandler = null;
47165
- /** ISR routes (from .tw/routes.json): pathname -> revalidate seconds. */
47493
+ /** ISR routes (from .tw/routes.json): pathname -> revalidate seconds
47494
+ * (bare number, legacy) or { revalidate, stale, expire, tag } object
47495
+ * (docs/cache-tags.md). Presence decides pipeline-vs-static routing. */
47166
47496
  isrRoutes = /* @__PURE__ */ new Map();
47167
47497
  securityHeaders = null;
47168
47498
  /**
@@ -47265,7 +47595,7 @@ var init_tw6 = __esm({
47265
47595
  if ((0, import_node_fs11.existsSync)(manifestPath)) {
47266
47596
  const manifest = JSON.parse((0, import_node_fs11.readFileSync)(manifestPath, "utf-8"));
47267
47597
  for (const [route, secs] of Object.entries(manifest)) {
47268
- this.isrRoutes.set(route, Number(secs));
47598
+ this.isrRoutes.set(route, secs);
47269
47599
  }
47270
47600
  if (Object.keys(manifest).length > 0) {
47271
47601
  console.log(` ISR: ${Object.keys(manifest).length} revalidate route(s)`);
@@ -47881,11 +48211,84 @@ __export(twm_loader_exports, {
47881
48211
  executeRouteHandler: () => executeRouteHandler,
47882
48212
  loadTWMModule: () => loadTWMModule,
47883
48213
  revalidateRoute: () => revalidateRoute,
47884
- shouldMatchMiddleware: () => shouldMatchMiddleware
48214
+ shouldMatchMiddleware: () => shouldMatchMiddleware,
48215
+ stripCommentsSafe: () => stripCommentsSafe
47885
48216
  });
47886
48217
  function clearTWMCache() {
47887
48218
  moduleCache.clear();
47888
48219
  }
48220
+ function stripCommentsSafe(src) {
48221
+ let out = "";
48222
+ let i = 0;
48223
+ let mode = "code";
48224
+ while (i < src.length) {
48225
+ const c = src[i];
48226
+ const n = i + 1 < src.length ? src[i + 1] : "";
48227
+ if (mode === "code") {
48228
+ if (c === "/" && n === "/") {
48229
+ mode = "line";
48230
+ i += 2;
48231
+ continue;
48232
+ }
48233
+ if (c === "/" && n === "*") {
48234
+ mode = "block";
48235
+ i += 2;
48236
+ continue;
48237
+ }
48238
+ if (c === "'") {
48239
+ mode = "sq";
48240
+ out += c;
48241
+ i++;
48242
+ continue;
48243
+ }
48244
+ if (c === '"') {
48245
+ mode = "dq";
48246
+ out += c;
48247
+ i++;
48248
+ continue;
48249
+ }
48250
+ if (c === "`") {
48251
+ mode = "tpl";
48252
+ out += c;
48253
+ i++;
48254
+ continue;
48255
+ }
48256
+ out += c;
48257
+ i++;
48258
+ continue;
48259
+ }
48260
+ if (mode === "line") {
48261
+ if (c === "\n") {
48262
+ mode = "code";
48263
+ out += c;
48264
+ }
48265
+ i++;
48266
+ continue;
48267
+ }
48268
+ if (mode === "block") {
48269
+ if (c === "*" && n === "/") {
48270
+ mode = "code";
48271
+ out += " ";
48272
+ i += 2;
48273
+ continue;
48274
+ }
48275
+ i++;
48276
+ continue;
48277
+ }
48278
+ out += c;
48279
+ if (c === "\\") {
48280
+ if (n) {
48281
+ out += n;
48282
+ i += 2;
48283
+ continue;
48284
+ }
48285
+ } else if (mode === "sq" && c === "'" || mode === "dq" && c === '"' || mode === "tpl" && c === "`") {
48286
+ mode = "code";
48287
+ }
48288
+ i++;
48289
+ }
48290
+ return out;
48291
+ }
47889
48292
  async function loadLibModule(rootDir, name) {
47890
48293
  let rel = name.replace(/^@\//, "").replace(/^\.\//, "").replace(/^lib\//, "");
47891
48294
  const cacheKey = rootDir + "/" + rel;
@@ -47904,13 +48307,13 @@ async function loadLibModule(rootDir, name) {
47904
48307
  }
47905
48308
  }
47906
48309
  function compileTwm(source) {
47907
- const cleaned = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*/gm, "");
48310
+ const cleaned = stripCommentsSafe(source);
47908
48311
  const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?/g;
47909
48312
  const codeWithoutImports = cleaned.replace(importRegex, "");
47910
- return codeWithoutImports.replace(/\bexport\s+(?=(async\s+)?(function|const|let|var|class))/g, "").replace(/\bfn\s+delete\b/g, "async function deleteFn").replace(/\bfn\s+/g, "async function ");
48313
+ return codeWithoutImports.replace(/\bexport\s+(?=(async\s+)?(function|const|let|var|class))/g, "").replace(/^[ \t]*cache\s*\{[^}]*\}[ \t]*;?[ \t]*$/gm, "").replace(/\bfn\s+cached\s+delete\b/g, "async function deleteFn").replace(/\bfn\s+cached\s+/g, "async function ").replace(/\bfn\s+delete\b/g, "async function deleteFn").replace(/\bfn\s+/g, "async function ");
47911
48314
  }
47912
48315
  function extractImports(source) {
47913
- const cleaned = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*/gm, "");
48316
+ const cleaned = stripCommentsSafe(source);
47914
48317
  const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?/g;
47915
48318
  const imports = [];
47916
48319
  let match;
@@ -47957,6 +48360,7 @@ async function loadTWMModule(filePath, rootDir) {
47957
48360
  for (const name of imp.names) {
47958
48361
  if (name === "revalidatePath") injected.revalidatePath = revalidatePath;
47959
48362
  else if (name === "revalidateRoute") injected.revalidateRoute = revalidateRoute;
48363
+ else if (name === "revalidateTag") injected.revalidateTag = revalidateTag;
47960
48364
  else if (name === "setSignal") injected.setSignal = setSignal;
47961
48365
  }
47962
48366
  try {
@@ -48015,6 +48419,41 @@ async function loadTWMModule(filePath, rootDir) {
48015
48419
  const actionNames = /* @__PURE__ */ new Set();
48016
48420
  for (const m of codeWithoutImports.matchAll(/\bfunction\s+(action[A-Za-z0-9_]*)\s*\(/g)) actionNames.add(m[1]);
48017
48421
  for (const m of codeWithoutImports.matchAll(/\b(?:const|let|var)\s+(action[A-Za-z0-9_]*)\s*=/g)) actionNames.add(m[1]);
48422
+ const cachedHandlers = {};
48423
+ const cleanedSource = stripCommentsSafe(source);
48424
+ const fnCachedRegex = /\bfn\s+cached\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)\s*\{/g;
48425
+ for (const cm of cleanedSource.matchAll(fnCachedRegex)) {
48426
+ const fnName = cm[1] === "delete" ? "deleteFn" : cm[1];
48427
+ const after = cleanedSource.slice((cm.index ?? 0) + cm[0].length);
48428
+ const bodyEnd = handlerBodyEnd(after);
48429
+ const body = after.slice(0, bodyEnd);
48430
+ const cacheBlock = /^\s*cache\s*\{([^}]*)\}/.exec(after);
48431
+ const meta = cacheBlock ? parseCacheBody(cacheBlock[1]) : {};
48432
+ if (meta.revalidate == null && !meta.life) {
48433
+ console.error("[tw] TW090: fn cached " + cm[1] + " in " + filePath + ' needs cache { revalidate N } or cache { life "profile" } -- not cached');
48434
+ continue;
48435
+ }
48436
+ let impure = null;
48437
+ for (const [pattern, label] of IMPURE_PATTERNS) {
48438
+ if (pattern.test(body)) {
48439
+ impure = label;
48440
+ break;
48441
+ }
48442
+ }
48443
+ if (impure) {
48444
+ console.error("[tw] TW091: fn cached " + cm[1] + " in " + filePath + " is impure (" + impure + ") -- handler excluded from caching");
48445
+ continue;
48446
+ }
48447
+ if (NONDET_PATTERN.test(body)) {
48448
+ console.warn("[tw] TW093: fn cached " + cm[1] + " in " + filePath + " calls a non-deterministic function -- the value freezes into the cache entry");
48449
+ }
48450
+ try {
48451
+ const resolved = resolveCache(meta, twmCacheProfiles(rd));
48452
+ if (resolved) cachedHandlers[fnName] = resolved;
48453
+ } catch (e) {
48454
+ console.error("[tw] " + e.message + " (" + filePath + ":" + cm[1] + ")");
48455
+ }
48456
+ }
48018
48457
  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(", ") + " };";
48019
48458
  const injectedKeys = Object.keys(injected);
48020
48459
  let module2 = {};
@@ -48040,6 +48479,7 @@ async function loadTWMModule(filePath, rootDir) {
48040
48479
  } catch (err2) {
48041
48480
  console.error("[twm-loader] Error parsing " + filePath + ":", err2);
48042
48481
  }
48482
+ module2.__twCached = cachedHandlers;
48043
48483
  moduleCache.set(cacheKey, module2);
48044
48484
  return module2;
48045
48485
  }
@@ -48074,6 +48514,39 @@ function sameOriginAllowed(req) {
48074
48514
  }
48075
48515
  function clearRouteCache() {
48076
48516
  routeCache.clear();
48517
+ handlerCache.clear();
48518
+ }
48519
+ function handlerBodyEnd(after) {
48520
+ let depth = 1;
48521
+ let inStr = null;
48522
+ for (let i = 0; i < after.length; i++) {
48523
+ const ch = after[i];
48524
+ if (inStr) {
48525
+ if (ch === "\\") i++;
48526
+ else if (ch === inStr) inStr = null;
48527
+ continue;
48528
+ }
48529
+ if (ch === '"' || ch === "'" || ch === "`") {
48530
+ inStr = ch;
48531
+ continue;
48532
+ }
48533
+ if (ch === "{") depth++;
48534
+ else if (ch === "}") {
48535
+ depth--;
48536
+ if (depth === 0) return i;
48537
+ }
48538
+ }
48539
+ return after.length;
48540
+ }
48541
+ function twmCacheProfiles(rootDir) {
48542
+ if (__twmProfiles.has(rootDir)) return __twmProfiles.get(rootDir);
48543
+ let profiles = {};
48544
+ try {
48545
+ profiles = readCacheProfilesSync(rootDir);
48546
+ } catch {
48547
+ }
48548
+ __twmProfiles.set(rootDir, profiles);
48549
+ return profiles;
48077
48550
  }
48078
48551
  function revalidateRoute(pathname, opts) {
48079
48552
  const want = pathname.startsWith("/") ? pathname : "/" + pathname;
@@ -48177,12 +48650,17 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
48177
48650
  if (!actionResult || typeof actionResult !== "object") {
48178
48651
  return { status: 500, json: { ok: false, error: "Internal Server Error" } };
48179
48652
  }
48653
+ let revalidatedTag;
48654
+ if (typeof actionResult.revalidateTag === "string" && actionResult.revalidateTag) {
48655
+ revalidatedTag = actionResult.revalidateTag;
48656
+ revalidateTag(revalidatedTag);
48657
+ }
48180
48658
  return {
48181
48659
  status: typeof actionResult.status === "number" ? actionResult.status : 200,
48182
48660
  json: actionResult.json || actionResult.body || {},
48183
48661
  html: typeof actionResult.html === "string" ? actionResult.html : void 0,
48184
48662
  text: typeof actionResult.text === "string" ? actionResult.text : void 0,
48185
- headers: actionResult.headers
48663
+ headers: revalidatedTag ? { ...actionResult.headers ?? {}, "x-tw-revalidated": revalidatedTag } : actionResult.headers
48186
48664
  };
48187
48665
  }
48188
48666
  const rv = mod.revalidate;
@@ -48203,6 +48681,57 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
48203
48681
  };
48204
48682
  }
48205
48683
  }
48684
+ const upperMethod = method.toUpperCase();
48685
+ let handlerConf = null;
48686
+ let handlerCacheKey = null;
48687
+ if ((upperMethod === "GET" || upperMethod === "HEAD") && mod.__twCached) {
48688
+ const handlerKey = methodLower === "delete" ? "deleteFn" : methodLower === "head" ? "get" : methodLower.toLowerCase();
48689
+ handlerConf = mod.__twCached[handlerKey] ?? null;
48690
+ if (handlerConf) {
48691
+ try {
48692
+ const u = new URL(request.url ?? "http://local");
48693
+ handlerCacheKey = sha256(
48694
+ upperMethod + "\n" + u.pathname + "\n" + canonicalizeQuery(u.search) + "\n" + JSON.stringify(request?.params ?? {})
48695
+ );
48696
+ } catch {
48697
+ handlerCacheKey = null;
48698
+ }
48699
+ }
48700
+ if (handlerCacheKey) {
48701
+ const hEntry = handlerCache.get(handlerCacheKey);
48702
+ const hNow = Date.now();
48703
+ if (hEntry && hNow < hEntry.freshUntil) {
48704
+ return {
48705
+ ...hEntry.result,
48706
+ headers: {
48707
+ ...hEntry.result.headers ?? {},
48708
+ "x-tw-cache": "HIT",
48709
+ "x-tw-cache-age": String(Math.max(0, Math.floor((hNow - hEntry.createdAt) / 1e3)))
48710
+ }
48711
+ };
48712
+ }
48713
+ if (hEntry && hNow < hEntry.expireAt && !pendingHandlerRefresh.has(handlerCacheKey)) {
48714
+ pendingHandlerRefresh.add(handlerCacheKey);
48715
+ void (async () => {
48716
+ try {
48717
+ await executeRouteHandler(filePath, method, request, rootDir);
48718
+ } catch {
48719
+ } finally {
48720
+ pendingHandlerRefresh.delete(handlerCacheKey);
48721
+ }
48722
+ })();
48723
+ return {
48724
+ ...hEntry.result,
48725
+ headers: {
48726
+ ...hEntry.result.headers ?? {},
48727
+ "x-tw-cache": "STALE",
48728
+ "x-tw-cache-age": String(Math.max(0, Math.floor((hNow - hEntry.createdAt) / 1e3)))
48729
+ }
48730
+ };
48731
+ }
48732
+ if (hEntry && hNow >= hEntry.expireAt) handlerCache.delete(handlerCacheKey);
48733
+ }
48734
+ }
48206
48735
  let result;
48207
48736
  try {
48208
48737
  result = await handler(req);
@@ -48230,6 +48759,33 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
48230
48759
  }
48231
48760
  routeCache.set(routeCacheKey, { result: finalResult, expiresAt: Date.now() + rv * 1e3, pathname: cachedPath });
48232
48761
  }
48762
+ if (handlerCacheKey && handlerConf && finalResult.status === 200) {
48763
+ const now = Date.now();
48764
+ let cachedPath = "";
48765
+ try {
48766
+ cachedPath = new URL(request.url ?? "http://local").pathname;
48767
+ } catch {
48768
+ cachedPath = "";
48769
+ }
48770
+ handlerCache.set(handlerCacheKey, {
48771
+ result: finalResult,
48772
+ freshUntil: now + handlerConf.revalidate * 1e3,
48773
+ expireAt: now + handlerConf.expire * 1e3,
48774
+ conf: handlerConf,
48775
+ createdAt: now,
48776
+ pathname: cachedPath
48777
+ });
48778
+ const cc = handlerConf.stale > 0 ? "public, max-age=" + handlerConf.stale + (Number.isFinite(handlerConf.expire) ? ", stale-while-revalidate=" + Math.max(0, handlerConf.expire - handlerConf.stale) : "") : null;
48779
+ return {
48780
+ ...finalResult,
48781
+ headers: {
48782
+ ...finalResult.headers ?? {},
48783
+ "x-tw-cache": "MISS",
48784
+ "x-tw-cache-age": "0",
48785
+ ...cc ? { "Cache-Control": cc } : {}
48786
+ }
48787
+ };
48788
+ }
48233
48789
  return finalResult;
48234
48790
  }
48235
48791
  async function executeMiddleware(filePath, request, rootDir) {
@@ -48268,7 +48824,7 @@ function shouldMatchMiddleware(matchers, pathname) {
48268
48824
  }
48269
48825
  return false;
48270
48826
  }
48271
- var import_node_fs12, import_node_path12, moduleCache, libCache, routeCache;
48827
+ var import_node_fs12, import_node_path12, moduleCache, libCache, routeCache, IMPURE_PATTERNS, NONDET_PATTERN, __twmProfiles, handlerCache, pendingHandlerRefresh;
48272
48828
  var init_twm_loader = __esm({
48273
48829
  "../../packages/server/tw/routing/twm-loader.ts"() {
48274
48830
  import_node_fs12 = require("node:fs");
@@ -48276,9 +48832,30 @@ var init_twm_loader = __esm({
48276
48832
  init_signal_stream();
48277
48833
  import_node_path12 = require("node:path");
48278
48834
  init_twm_rules();
48835
+ init_tw();
48279
48836
  moduleCache = /* @__PURE__ */ new Map();
48280
48837
  libCache = /* @__PURE__ */ new Map();
48281
48838
  routeCache = /* @__PURE__ */ new Map();
48839
+ IMPURE_PATTERNS = [
48840
+ [/request\s*\.\s*cookies/, "request.cookies"],
48841
+ [/request\s*\.\s*headers/, "request.headers"],
48842
+ [/request\s*\.\s*body/, "request.body"],
48843
+ [/\bsetSignal\s*\(/, "setSignal(...)"]
48844
+ ];
48845
+ NONDET_PATTERN = /\b(?:Date\s*\.\s*now\s*\(|Math\s*\.\s*random\s*\(|crypto\s*\.\s*randomUUID\s*\()/;
48846
+ __twmProfiles = /* @__PURE__ */ new Map();
48847
+ handlerCache = /* @__PURE__ */ new Map();
48848
+ pendingHandlerRefresh = /* @__PURE__ */ new Set();
48849
+ registerTwmTagInvalidator(function(tag) {
48850
+ let dropped = 0;
48851
+ for (const [key, entry] of handlerCache) {
48852
+ if (entry.conf && entry.conf.tag === tag) {
48853
+ handlerCache.delete(key);
48854
+ dropped++;
48855
+ }
48856
+ }
48857
+ return dropped;
48858
+ });
48282
48859
  }
48283
48860
  });
48284
48861