tw-framework 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hydration-runtime.js +19 -3
- package/dist/testing.cjs +1058 -284
- package/dist/tw-server.mjs +56209 -0
- package/dist/tw.mjs +1391 -361
- package/package.json +7 -2
package/dist/tw.mjs
CHANGED
|
@@ -13372,6 +13372,13 @@ function consumeSignalDeclaration(cursor) {
|
|
|
13372
13372
|
function consumeStateValue(cursor) {
|
|
13373
13373
|
const t2 = cursor.peek();
|
|
13374
13374
|
if (!t2) return "";
|
|
13375
|
+
if (t2.value === "-" || t2.value === "+") {
|
|
13376
|
+
const after = cursor.peek(1);
|
|
13377
|
+
if (after && (after.type === "NUMBER" || after.type === "IDENT" || after.type === "KEYWORD")) {
|
|
13378
|
+
cursor.advance();
|
|
13379
|
+
return (t2.value === "-" ? "-" : "") + String(after.value);
|
|
13380
|
+
}
|
|
13381
|
+
}
|
|
13375
13382
|
if (t2.type === "STRING" || t2.type === "NUMBER" || t2.type === "IDENT" || t2.type === "KEYWORD") {
|
|
13376
13383
|
cursor.advance();
|
|
13377
13384
|
if (t2.type === "STRING") return String(t2.value).replace(/\\([{}"])/g, "$1");
|
|
@@ -14389,6 +14396,20 @@ function parseTWSelector(cursor, errors, state, parseBody) {
|
|
|
14389
14396
|
}
|
|
14390
14397
|
cursor.consumeIf("RBRACE");
|
|
14391
14398
|
} else {
|
|
14399
|
+
for (; ; ) {
|
|
14400
|
+
const t2 = cursor.peek();
|
|
14401
|
+
if (!t2 || t2.type === "RBRACE" || t2.type === "EOF") break;
|
|
14402
|
+
if ((t2.type === "IDENT" || t2.type === "KEYWORD") && ATTR_PAIR_NAMES.has(t2.value)) {
|
|
14403
|
+
const after = cursor.peek(1);
|
|
14404
|
+
if (after && (after.type === "STRING" || after.type === "NUMBER")) {
|
|
14405
|
+
cursor.advance();
|
|
14406
|
+
cursor.advance();
|
|
14407
|
+
el.attrs.push(createAttribute(t2.value, after.value, t2.pos.line, t2.pos.col));
|
|
14408
|
+
continue;
|
|
14409
|
+
}
|
|
14410
|
+
}
|
|
14411
|
+
break;
|
|
14412
|
+
}
|
|
14392
14413
|
el.children = parseBody(["RBRACE"]);
|
|
14393
14414
|
cursor.consumeIf("RBRACE");
|
|
14394
14415
|
}
|
|
@@ -15007,10 +15028,13 @@ function parseWhileStatement(cursor, errors, parseBody) {
|
|
|
15007
15028
|
const whileToken = cursor.advance();
|
|
15008
15029
|
if (!whileToken) return null;
|
|
15009
15030
|
cursor.skipWhitespace();
|
|
15010
|
-
const condToken = cursor.peek();
|
|
15011
15031
|
let condition = "";
|
|
15012
|
-
|
|
15013
|
-
|
|
15032
|
+
while (!cursor.done) {
|
|
15033
|
+
cursor.skipWhitespace();
|
|
15034
|
+
const t2 = cursor.peek();
|
|
15035
|
+
if (!t2 || t2.type === "EOF") break;
|
|
15036
|
+
if (t2.type === "LBRACE" || t2.value === "{") break;
|
|
15037
|
+
condition += (condition ? " " : "") + t2.value;
|
|
15014
15038
|
cursor.advance();
|
|
15015
15039
|
}
|
|
15016
15040
|
const body = parseBlockBody2(cursor, parseBody);
|
|
@@ -15131,13 +15155,133 @@ function parseBlockBody2(cursor, parseBody) {
|
|
|
15131
15155
|
}
|
|
15132
15156
|
return parseBody(["KEYWORD", "RBRACE"]);
|
|
15133
15157
|
}
|
|
15134
|
-
var CLASS_BOUNDARY_BOOLEAN_ATTRS;
|
|
15158
|
+
var ATTR_PAIR_NAMES, CLASS_BOUNDARY_BOOLEAN_ATTRS;
|
|
15135
15159
|
var init_statements = __esm({
|
|
15136
15160
|
"../../packages/compiler/tw/parser/statements/index.ts"() {
|
|
15137
15161
|
init_nodes();
|
|
15138
15162
|
init_recovery();
|
|
15139
15163
|
init_directives();
|
|
15140
15164
|
init_tw();
|
|
15165
|
+
ATTR_PAIR_NAMES = /* @__PURE__ */ new Set([
|
|
15166
|
+
"href",
|
|
15167
|
+
"src",
|
|
15168
|
+
"type",
|
|
15169
|
+
"id",
|
|
15170
|
+
"name",
|
|
15171
|
+
"value",
|
|
15172
|
+
"placeholder",
|
|
15173
|
+
"action",
|
|
15174
|
+
"method",
|
|
15175
|
+
"title",
|
|
15176
|
+
"alt",
|
|
15177
|
+
"rel",
|
|
15178
|
+
"target",
|
|
15179
|
+
"lang",
|
|
15180
|
+
"charset",
|
|
15181
|
+
"content",
|
|
15182
|
+
"width",
|
|
15183
|
+
"height",
|
|
15184
|
+
"loading",
|
|
15185
|
+
"for",
|
|
15186
|
+
"role",
|
|
15187
|
+
"min",
|
|
15188
|
+
"max",
|
|
15189
|
+
"step",
|
|
15190
|
+
"pattern",
|
|
15191
|
+
"accept",
|
|
15192
|
+
"list",
|
|
15193
|
+
"download",
|
|
15194
|
+
"tabindex",
|
|
15195
|
+
"cols",
|
|
15196
|
+
"rows",
|
|
15197
|
+
"wrap",
|
|
15198
|
+
"datetime",
|
|
15199
|
+
"method",
|
|
15200
|
+
"action",
|
|
15201
|
+
"placeholder",
|
|
15202
|
+
"disabled",
|
|
15203
|
+
"required",
|
|
15204
|
+
"readonly",
|
|
15205
|
+
"checked",
|
|
15206
|
+
"selected",
|
|
15207
|
+
"target",
|
|
15208
|
+
"rel",
|
|
15209
|
+
"download",
|
|
15210
|
+
"autocomplete",
|
|
15211
|
+
"autofocus",
|
|
15212
|
+
"min",
|
|
15213
|
+
"max",
|
|
15214
|
+
"step",
|
|
15215
|
+
"pattern",
|
|
15216
|
+
"multiple",
|
|
15217
|
+
"size",
|
|
15218
|
+
"cols",
|
|
15219
|
+
"rows",
|
|
15220
|
+
"wrap",
|
|
15221
|
+
"for",
|
|
15222
|
+
"hidden",
|
|
15223
|
+
"width",
|
|
15224
|
+
"height",
|
|
15225
|
+
"loading",
|
|
15226
|
+
"loop",
|
|
15227
|
+
"muted",
|
|
15228
|
+
"controls",
|
|
15229
|
+
"poster",
|
|
15230
|
+
"preload",
|
|
15231
|
+
"contenteditable",
|
|
15232
|
+
"draggable",
|
|
15233
|
+
"spellcheck",
|
|
15234
|
+
"translate",
|
|
15235
|
+
"accesskey",
|
|
15236
|
+
"tabindex",
|
|
15237
|
+
"role",
|
|
15238
|
+
"slot",
|
|
15239
|
+
"is",
|
|
15240
|
+
"part",
|
|
15241
|
+
"enterkeyhint",
|
|
15242
|
+
"inert",
|
|
15243
|
+
"inputmode",
|
|
15244
|
+
"sandbox",
|
|
15245
|
+
"allow",
|
|
15246
|
+
"allowfullscreen",
|
|
15247
|
+
"referrerpolicy",
|
|
15248
|
+
"fetchpriority",
|
|
15249
|
+
"blocking",
|
|
15250
|
+
"property",
|
|
15251
|
+
"itemprop",
|
|
15252
|
+
"itemtype",
|
|
15253
|
+
"itemid",
|
|
15254
|
+
"itemref",
|
|
15255
|
+
"itemscope",
|
|
15256
|
+
"charset",
|
|
15257
|
+
"http-equiv",
|
|
15258
|
+
"data",
|
|
15259
|
+
"nonce",
|
|
15260
|
+
"crossorigin",
|
|
15261
|
+
"integrity",
|
|
15262
|
+
"async",
|
|
15263
|
+
"defer",
|
|
15264
|
+
"srcset",
|
|
15265
|
+
"sizes",
|
|
15266
|
+
"media",
|
|
15267
|
+
"ping",
|
|
15268
|
+
"coords",
|
|
15269
|
+
"shape",
|
|
15270
|
+
"usemap",
|
|
15271
|
+
"ismap",
|
|
15272
|
+
"kind",
|
|
15273
|
+
"srclang",
|
|
15274
|
+
"default",
|
|
15275
|
+
"autoplay",
|
|
15276
|
+
"playsinline",
|
|
15277
|
+
"enctype",
|
|
15278
|
+
"novalidate",
|
|
15279
|
+
"formaction",
|
|
15280
|
+
"formmethod",
|
|
15281
|
+
"formtarget",
|
|
15282
|
+
"formenctype",
|
|
15283
|
+
"formnovalidate"
|
|
15284
|
+
]);
|
|
15141
15285
|
CLASS_BOUNDARY_BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
|
|
15142
15286
|
"disabled",
|
|
15143
15287
|
"required",
|
|
@@ -22950,7 +23094,7 @@ var init_types4 = __esm({
|
|
|
22950
23094
|
var TW_VERSION, TW_GENERATOR_META;
|
|
22951
23095
|
var init_version = __esm({
|
|
22952
23096
|
"../../packages/compiler/tw/codegen/version.ts"() {
|
|
22953
|
-
TW_VERSION = "1.0.
|
|
23097
|
+
TW_VERSION = "1.0.8";
|
|
22954
23098
|
TW_GENERATOR_META = `<meta name="generator" content="TW Framework ${TW_VERSION}">`;
|
|
22955
23099
|
}
|
|
22956
23100
|
});
|
|
@@ -23337,6 +23481,16 @@ function collectBuiltinImports(program) {
|
|
|
23337
23481
|
for (const dir of program.directives ?? []) {
|
|
23338
23482
|
if (dir.type !== "ImportDirective") continue;
|
|
23339
23483
|
const imp = dir;
|
|
23484
|
+
if (imp.source === "tw" || imp.source === "@tw/server") {
|
|
23485
|
+
for (const item of imp.items ?? []) {
|
|
23486
|
+
const name = item.split(" as ").pop().trim();
|
|
23487
|
+
const spec = TW_NAMED_BUILTINS[name];
|
|
23488
|
+
if (spec) out.set(name, spec);
|
|
23489
|
+
}
|
|
23490
|
+
if (imp.defaultImport && TW_NAMED_BUILTINS[imp.defaultImport]) {
|
|
23491
|
+
out.set(imp.defaultImport, TW_NAMED_BUILTINS[imp.defaultImport]);
|
|
23492
|
+
}
|
|
23493
|
+
}
|
|
23340
23494
|
if (!BUILTIN_COMPONENT_SPECIFIERS.includes(imp.source)) continue;
|
|
23341
23495
|
if (imp.defaultImport) out.set(imp.defaultImport, imp.source);
|
|
23342
23496
|
for (const item of imp.items ?? []) {
|
|
@@ -23406,7 +23560,7 @@ function normalizePrefetchProp(props) {
|
|
|
23406
23560
|
return p;
|
|
23407
23561
|
}
|
|
23408
23562
|
function generateRouterLinkTag(props, textContent) {
|
|
23409
|
-
const href = (props.href ?? "").trim();
|
|
23563
|
+
const href = (props.href ?? props.to ?? "").trim();
|
|
23410
23564
|
if (!href) return `<!-- @tw/RouterLink: missing href -->`;
|
|
23411
23565
|
const opts = {
|
|
23412
23566
|
href,
|
|
@@ -23433,13 +23587,18 @@ function generateBuiltinTag(specifier, props, textContent) {
|
|
|
23433
23587
|
if (specifier === "@tw/RouterLink") return generateRouterLinkTag(props, textContent);
|
|
23434
23588
|
return null;
|
|
23435
23589
|
}
|
|
23436
|
-
var BUILTIN_COMPONENT_SPECIFIERS, imageConfig;
|
|
23590
|
+
var BUILTIN_COMPONENT_SPECIFIERS, imageConfig, TW_NAMED_BUILTINS;
|
|
23437
23591
|
var init_builtin_components = __esm({
|
|
23438
23592
|
"../../packages/compiler/tw/codegen/builtin-components.ts"() {
|
|
23439
23593
|
init_tw2();
|
|
23440
23594
|
init_tw3();
|
|
23441
23595
|
BUILTIN_COMPONENT_SPECIFIERS = ["@tw/optImage", "@tw/RouterLink"];
|
|
23442
23596
|
imageConfig = DEFAULT_IMAGE_CONFIG;
|
|
23597
|
+
TW_NAMED_BUILTINS = {
|
|
23598
|
+
RouterLink: "@tw/RouterLink",
|
|
23599
|
+
optImage: "@tw/optImage",
|
|
23600
|
+
Image: "@tw/optImage"
|
|
23601
|
+
};
|
|
23443
23602
|
}
|
|
23444
23603
|
});
|
|
23445
23604
|
|
|
@@ -24076,6 +24235,58 @@ function stripTssComments(src) {
|
|
|
24076
24235
|
return out;
|
|
24077
24236
|
}
|
|
24078
24237
|
function compileTSS(source) {
|
|
24238
|
+
{
|
|
24239
|
+
let depth = 0;
|
|
24240
|
+
let line = 1;
|
|
24241
|
+
let inString = null;
|
|
24242
|
+
let inComment = false;
|
|
24243
|
+
let inUrl = false;
|
|
24244
|
+
for (let i = 0; i < source.length; i++) {
|
|
24245
|
+
const ch = source[i];
|
|
24246
|
+
const prev = i > 0 ? source[i - 1] : "";
|
|
24247
|
+
if (ch === "\n") {
|
|
24248
|
+
line++;
|
|
24249
|
+
inComment = false;
|
|
24250
|
+
continue;
|
|
24251
|
+
}
|
|
24252
|
+
if (inComment) continue;
|
|
24253
|
+
if (inString) {
|
|
24254
|
+
if (ch === inString && prev !== "\\") inString = null;
|
|
24255
|
+
continue;
|
|
24256
|
+
}
|
|
24257
|
+
if (inUrl) {
|
|
24258
|
+
if (ch === ")") inUrl = false;
|
|
24259
|
+
continue;
|
|
24260
|
+
}
|
|
24261
|
+
if (ch === "/" && source[i + 1] === "/" && source.slice(Math.max(0, i - 3), i).includes(":")) {
|
|
24262
|
+
inUrl = true;
|
|
24263
|
+
continue;
|
|
24264
|
+
}
|
|
24265
|
+
if (ch === "u" && source.slice(i, i + 4) === "url(") {
|
|
24266
|
+
inUrl = true;
|
|
24267
|
+
i += 3;
|
|
24268
|
+
continue;
|
|
24269
|
+
}
|
|
24270
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
24271
|
+
inComment = true;
|
|
24272
|
+
continue;
|
|
24273
|
+
}
|
|
24274
|
+
if (ch === '"' || ch === "'") {
|
|
24275
|
+
inString = ch;
|
|
24276
|
+
continue;
|
|
24277
|
+
}
|
|
24278
|
+
if (ch === "{") depth++;
|
|
24279
|
+
else if (ch === "}") {
|
|
24280
|
+
depth--;
|
|
24281
|
+
if (depth < 0) {
|
|
24282
|
+
throw new Error("TW303: unbalanced '}' in stylesheet (line " + line + ")\n " + (source.split("\n")[line - 1] ?? "").trim().slice(0, 80));
|
|
24283
|
+
}
|
|
24284
|
+
}
|
|
24285
|
+
}
|
|
24286
|
+
if (depth !== 0) {
|
|
24287
|
+
throw new Error("TW303: " + depth + " unclosed '{' in stylesheet -- a rule block is never closed\n last line: " + (source.split("\n").pop() ?? "").trim().slice(0, 80));
|
|
24288
|
+
}
|
|
24289
|
+
}
|
|
24079
24290
|
const __twOut = __twCompileTSSInner(source);
|
|
24080
24291
|
const leftover = __twOut.match(/\$[A-Za-z_][\w-]*/g);
|
|
24081
24292
|
if (leftover && !/\$\{/.test(__twOut)) {
|
|
@@ -24620,11 +24831,11 @@ function emit(items, parent, env, mixins, out) {
|
|
|
24620
24831
|
}
|
|
24621
24832
|
function compileSCSS(source) {
|
|
24622
24833
|
const cleaned0 = stripComments(source);
|
|
24623
|
-
const UNSUPPORTED = /@(import|
|
|
24834
|
+
const UNSUPPORTED = /@(import|extend|use|forward|function|each|if|else|for|while)\b/g;
|
|
24624
24835
|
const m = cleaned0.match(UNSUPPORTED);
|
|
24625
24836
|
if (m) {
|
|
24626
24837
|
const seen = Array.from(new Set(m.map((x) => x.trim())));
|
|
24627
|
-
console.warn("[scss] unsupported at-rule(s) ignored: " + seen.join(", ") + " -- TW's SCSS subset supports nesting, $vars
|
|
24838
|
+
console.warn("[scss] unsupported at-rule(s) ignored: " + seen.join(", ") + " -- TW's SCSS subset supports nesting, $vars, & references and @mixin/@include");
|
|
24628
24839
|
}
|
|
24629
24840
|
const items = parseItems(stripComments(source));
|
|
24630
24841
|
const out = [];
|
|
@@ -24766,13 +24977,13 @@ function collectTssImports(programs, ctx) {
|
|
|
24766
24977
|
else ctx.inlineStyles.push(css);
|
|
24767
24978
|
}
|
|
24768
24979
|
} catch (e) {
|
|
24769
|
-
if (e && typeof e.message === "string" && /^TW30[
|
|
24980
|
+
if (e && typeof e.message === "string" && /^TW30[123]/.test(e.message)) throw e;
|
|
24770
24981
|
}
|
|
24771
24982
|
}
|
|
24772
24983
|
}
|
|
24773
24984
|
}
|
|
24774
24985
|
} catch (e) {
|
|
24775
|
-
if (e && typeof e.message === "string" && /^TW30[
|
|
24986
|
+
if (e && typeof e.message === "string" && /^TW30[123]/.test(e.message)) throw e;
|
|
24776
24987
|
}
|
|
24777
24988
|
}
|
|
24778
24989
|
function resolveTssPath(fromDir, rel) {
|
|
@@ -24992,6 +25203,10 @@ function generateSuspense(el, ctx) {
|
|
|
24992
25203
|
function generateElement(el, ctx) {
|
|
24993
25204
|
const tag = el.tag;
|
|
24994
25205
|
const voidTag = VOID_TAGS5.has(tag.toLowerCase());
|
|
25206
|
+
if (tag.length > 0 && tag[0] === tag[0].toUpperCase() && !["Link", "Suspense"].includes(tag) && !componentRegistry.has(tag) && !resolveBuiltin(tag, activeBuiltinImports)) {
|
|
25207
|
+
console.warn("[tw] unknown component <" + tag + "> (" + el.line + ":" + el.col + ") -- no import resolves it; nothing was rendered");
|
|
25208
|
+
return "<!-- TW: unknown component '" + tag + "' -- check the import -->";
|
|
25209
|
+
}
|
|
24995
25210
|
{
|
|
24996
25211
|
const builtinSpec = resolveBuiltin(tag, activeBuiltinImports);
|
|
24997
25212
|
if (builtinSpec) {
|
|
@@ -25251,7 +25466,13 @@ function generateComponent(comp, ctx) {
|
|
|
25251
25466
|
ctx.slotContent = savedSlot;
|
|
25252
25467
|
ctx.inHead = compSavedInHead;
|
|
25253
25468
|
} else {
|
|
25254
|
-
|
|
25469
|
+
const known = ["Link", "Suspense"];
|
|
25470
|
+
if (!known.includes(compName)) {
|
|
25471
|
+
console.warn("[tw] unknown component <" + compName + "> (" + comp.line + ":" + comp.col + ") -- unresolvable import; nothing was rendered");
|
|
25472
|
+
childrenHTML = "<!-- TW: unknown component '" + compName + "' -- check the import -->";
|
|
25473
|
+
} else {
|
|
25474
|
+
childrenHTML = comp.children.map((c) => generateNode(c, ctx)).join("");
|
|
25475
|
+
}
|
|
25255
25476
|
}
|
|
25256
25477
|
ctx.stateVars = savedStateVars;
|
|
25257
25478
|
ctx.componentStack.pop();
|
|
@@ -29046,13 +29267,27 @@ ${indentation})` : type;
|
|
|
29046
29267
|
});
|
|
29047
29268
|
|
|
29048
29269
|
// ../../packages/compiler/tw/codegen/vdom.ts
|
|
29270
|
+
function stateLiteralForRender(raw) {
|
|
29271
|
+
const evalNum = (v) => typeof v === "number" || typeof v === "boolean" ? String(v) : null;
|
|
29272
|
+
try {
|
|
29273
|
+
const n = evalNum(JSON.parse(raw));
|
|
29274
|
+
if (n !== null) return n;
|
|
29275
|
+
} catch {
|
|
29276
|
+
}
|
|
29277
|
+
try {
|
|
29278
|
+
const n = evalNum(new Function("return (" + raw + ")")());
|
|
29279
|
+
if (n !== null) return n;
|
|
29280
|
+
} catch {
|
|
29281
|
+
}
|
|
29282
|
+
return raw;
|
|
29283
|
+
}
|
|
29049
29284
|
function generateWithLayout(layoutProgram, pageProgram, stateVars) {
|
|
29050
29285
|
const ctx = createContext("ssr");
|
|
29051
29286
|
if (stateVars) ctx.stateVars = { ...stateVars };
|
|
29052
29287
|
for (const dir of pageProgram.directives || []) {
|
|
29053
29288
|
if (dir.type === "StateDirective") {
|
|
29054
29289
|
for (const decl of dir.declarations || []) {
|
|
29055
|
-
ctx.stateVars[decl.name] = decl.value;
|
|
29290
|
+
ctx.stateVars[decl.name] = stateLiteralForRender(decl.value);
|
|
29056
29291
|
}
|
|
29057
29292
|
}
|
|
29058
29293
|
}
|
|
@@ -29084,7 +29319,7 @@ function generateWithLayoutChain(layoutPrograms, pageProgram, stateVars) {
|
|
|
29084
29319
|
for (const dir of pageProgram.directives || []) {
|
|
29085
29320
|
if (dir.type === "StateDirective") {
|
|
29086
29321
|
for (const decl of dir.declarations || []) {
|
|
29087
|
-
ctx2.stateVars[decl.name] = decl.value;
|
|
29322
|
+
ctx2.stateVars[decl.name] = stateLiteralForRender(decl.value);
|
|
29088
29323
|
}
|
|
29089
29324
|
}
|
|
29090
29325
|
}
|
|
@@ -29275,7 +29510,9 @@ function generate(program, stateVars) {
|
|
|
29275
29510
|
ctx.hasInteractivity = true;
|
|
29276
29511
|
continue;
|
|
29277
29512
|
}
|
|
29278
|
-
|
|
29513
|
+
if (!(decl.name in ctx.stateVars)) {
|
|
29514
|
+
ctx.stateVars[decl.name] = stateLiteralForRender(decl.value);
|
|
29515
|
+
}
|
|
29279
29516
|
let v = decl.value;
|
|
29280
29517
|
try {
|
|
29281
29518
|
v = JSON.parse(decl.value);
|
|
@@ -29608,7 +29845,11 @@ var init_codes = __esm({
|
|
|
29608
29845
|
TW090: { message: "cache { } requires a revalidate window or a life profile", severity: "error", category: "syntax" },
|
|
29609
29846
|
TW091: { message: "fn cached handler is impure (request.cookies/headers/body or setSignal)", severity: "error", category: "semantic" },
|
|
29610
29847
|
TW092: { message: "Unknown cache profile", severity: "error", category: "semantic" },
|
|
29611
|
-
TW093: { message: "Non-deterministic call in a cached handler (value freezes into the cache entry)", severity: "warning", category: "best-practice" }
|
|
29848
|
+
TW093: { message: "Non-deterministic call in a cached handler (value freezes into the cache entry)", severity: "warning", category: "best-practice" },
|
|
29849
|
+
TW094: { message: "Invalid state literal -- array/object elements must be comma-separated (and objects need colons)", severity: "error", category: "syntax" },
|
|
29850
|
+
TW095: { message: "Invalid for-loop iterable -- use {varName} or a valid comma-separated literal", severity: "error", category: "syntax" },
|
|
29851
|
+
TW096: { message: "Unknown state variable in interpolation (renders empty)", severity: "warning", category: "semantic" },
|
|
29852
|
+
TW097: { message: "while with a comparison renders its body ONCE at build time (state cannot mutate during SSR) -- use for loops for repetition", severity: "warning", category: "semantic" }
|
|
29612
29853
|
};
|
|
29613
29854
|
}
|
|
29614
29855
|
});
|
|
@@ -29929,6 +30170,124 @@ function checkUnusedDeclarations(program, filePath, diags) {
|
|
|
29929
30170
|
}
|
|
29930
30171
|
}
|
|
29931
30172
|
}
|
|
30173
|
+
function literalIsBroken(value) {
|
|
30174
|
+
const v = value.trim();
|
|
30175
|
+
if (!v.startsWith("[") && !v.startsWith("{")) return false;
|
|
30176
|
+
try {
|
|
30177
|
+
new Function("return (" + v + ")");
|
|
30178
|
+
return false;
|
|
30179
|
+
} catch {
|
|
30180
|
+
return true;
|
|
30181
|
+
}
|
|
30182
|
+
}
|
|
30183
|
+
function checkStateLiterals(program, filePath, diags) {
|
|
30184
|
+
for (const decl of collectStateDecls(program)) {
|
|
30185
|
+
if (decl.signalKind === "derived") continue;
|
|
30186
|
+
if (literalIsBroken(String(decl.value))) {
|
|
30187
|
+
diags.push(createDiagnostic(
|
|
30188
|
+
"TW094",
|
|
30189
|
+
decl.line,
|
|
30190
|
+
decl.col,
|
|
30191
|
+
filePath,
|
|
30192
|
+
`${decl.name} = ${decl.value}`,
|
|
30193
|
+
[
|
|
30194
|
+
`Comma-separate array elements: ${decl.name} = ["a", "b", "c"]`,
|
|
30195
|
+
`Objects need colons: ${decl.name} = { key: "value" }`
|
|
30196
|
+
]
|
|
30197
|
+
));
|
|
30198
|
+
}
|
|
30199
|
+
}
|
|
30200
|
+
}
|
|
30201
|
+
function checkLoopIterables(program, filePath, diags) {
|
|
30202
|
+
forEachNode(program, (node) => {
|
|
30203
|
+
if (!node || node.type !== "For") return;
|
|
30204
|
+
const iterable = String(node.iterable ?? "").trim();
|
|
30205
|
+
if (!iterable) {
|
|
30206
|
+
diags.push(createDiagnostic(
|
|
30207
|
+
"TW095",
|
|
30208
|
+
node.line,
|
|
30209
|
+
node.col,
|
|
30210
|
+
filePath,
|
|
30211
|
+
"for-loop has no iterable",
|
|
30212
|
+
[`Use: for item in {list} { ... }`]
|
|
30213
|
+
));
|
|
30214
|
+
return;
|
|
30215
|
+
}
|
|
30216
|
+
if (/^\{[A-Za-z_][\w.]*\}$/.test(iterable)) return;
|
|
30217
|
+
if (/^[A-Za-z_][\w.]*$/.test(iterable)) return;
|
|
30218
|
+
if (literalIsBroken(iterable)) {
|
|
30219
|
+
diags.push(createDiagnostic(
|
|
30220
|
+
"TW095",
|
|
30221
|
+
node.line,
|
|
30222
|
+
node.col,
|
|
30223
|
+
filePath,
|
|
30224
|
+
"for x in " + iterable,
|
|
30225
|
+
[
|
|
30226
|
+
`Wrap a state variable in braces: for item in {items}`,
|
|
30227
|
+
`Literals need commas: for item in {["a", "b", "c"]}`
|
|
30228
|
+
]
|
|
30229
|
+
));
|
|
30230
|
+
}
|
|
30231
|
+
});
|
|
30232
|
+
}
|
|
30233
|
+
function checkUnknownStateVars(program, filePath, diags) {
|
|
30234
|
+
const known = /* @__PURE__ */ new Set();
|
|
30235
|
+
for (const decl of collectStateDecls(program)) known.add(decl.name);
|
|
30236
|
+
forEachNode(program, (node) => {
|
|
30237
|
+
if (node && node.type === "For" && node.varName) known.add(node.varName);
|
|
30238
|
+
if (node && node.type === "For" && node.indexName) known.add(node.indexName);
|
|
30239
|
+
});
|
|
30240
|
+
const BUILTINS2 = /* @__PURE__ */ new Set([
|
|
30241
|
+
"params",
|
|
30242
|
+
"props",
|
|
30243
|
+
"Math",
|
|
30244
|
+
"Date",
|
|
30245
|
+
"JSON",
|
|
30246
|
+
"String",
|
|
30247
|
+
"Number",
|
|
30248
|
+
"Boolean",
|
|
30249
|
+
"Array",
|
|
30250
|
+
"Object",
|
|
30251
|
+
"window",
|
|
30252
|
+
"true",
|
|
30253
|
+
"false",
|
|
30254
|
+
"null",
|
|
30255
|
+
"undefined"
|
|
30256
|
+
]);
|
|
30257
|
+
forEachNode(program, (node) => {
|
|
30258
|
+
if (!isText(node)) return;
|
|
30259
|
+
const text = String(node.value ?? "");
|
|
30260
|
+
if (!text.includes("{")) return;
|
|
30261
|
+
for (const m of text.matchAll(/\{\s*([A-Za-z_][\w]*)\s*\}/g)) {
|
|
30262
|
+
const name = m[1];
|
|
30263
|
+
if (known.has(name) || BUILTINS2.has(name)) continue;
|
|
30264
|
+
diags.push(createDiagnostic(
|
|
30265
|
+
"TW096",
|
|
30266
|
+
node.line,
|
|
30267
|
+
node.col,
|
|
30268
|
+
filePath,
|
|
30269
|
+
`{${name}}`,
|
|
30270
|
+
[`Declare it in the state block, or check the spelling`]
|
|
30271
|
+
));
|
|
30272
|
+
}
|
|
30273
|
+
});
|
|
30274
|
+
}
|
|
30275
|
+
function checkWhileComparisons(program, filePath, diags) {
|
|
30276
|
+
forEachNode(program, (node) => {
|
|
30277
|
+
if (!node || node.type !== "While") return;
|
|
30278
|
+
const cond = String(node.condition ?? "");
|
|
30279
|
+
if (/[<>=!]/.test(cond)) {
|
|
30280
|
+
diags.push(createDiagnostic(
|
|
30281
|
+
"TW097",
|
|
30282
|
+
node.line,
|
|
30283
|
+
node.col,
|
|
30284
|
+
filePath,
|
|
30285
|
+
"while " + cond,
|
|
30286
|
+
["Use `for item in {list}` for repetition", "while is for boolean flags (state { loading = true })"]
|
|
30287
|
+
));
|
|
30288
|
+
}
|
|
30289
|
+
});
|
|
30290
|
+
}
|
|
29932
30291
|
var HTML_ELEMENTS2, HTML_ATTRIBUTES2, GLOBAL_ATTRIBUTES2, CSS_PROPERTIES2, CSS_ALIASES2, EVENT_TYPES2, ARIA_ROLES2, ARIA_ATTRIBUTES2;
|
|
29933
30292
|
var init_rules = __esm({
|
|
29934
30293
|
"../../packages/compiler/tw/diagnostics/rules.ts"() {
|
|
@@ -30819,6 +31178,10 @@ function diagnose(program, filePath) {
|
|
|
30819
31178
|
checkSemanticErrors(program, filePath, diagnostics);
|
|
30820
31179
|
checkDirectives(program, filePath, diagnostics);
|
|
30821
31180
|
checkUnusedDeclarations(program, filePath, diagnostics);
|
|
31181
|
+
checkStateLiterals(program, filePath, diagnostics);
|
|
31182
|
+
checkLoopIterables(program, filePath, diagnostics);
|
|
31183
|
+
checkUnknownStateVars(program, filePath, diagnostics);
|
|
31184
|
+
checkWhileComparisons(program, filePath, diagnostics);
|
|
30822
31185
|
return diagnostics;
|
|
30823
31186
|
}
|
|
30824
31187
|
var RuleEngine;
|
|
@@ -40443,6 +40806,8 @@ function matchPath(pattern, pathname) {
|
|
|
40443
40806
|
}
|
|
40444
40807
|
function ruleBlocked(rule, request, pathname) {
|
|
40445
40808
|
const headers = request.headers;
|
|
40809
|
+
const hasCondition = rule.userAgent || rule.path || rule.auth || rule.rateLimit || rule.origin || rule.methods && rule.methods.length > 0;
|
|
40810
|
+
if (!hasCondition) return true;
|
|
40446
40811
|
if (rule.userAgent) {
|
|
40447
40812
|
const ua = (headers.get("user-agent") ?? "").toLowerCase();
|
|
40448
40813
|
if (!ua) {
|
|
@@ -41011,6 +41376,15 @@ var init_revalidate = __esm({
|
|
|
41011
41376
|
});
|
|
41012
41377
|
|
|
41013
41378
|
// ../../packages/server/tw/routing/signal-stream.ts
|
|
41379
|
+
var signal_stream_exports = {};
|
|
41380
|
+
__export(signal_stream_exports, {
|
|
41381
|
+
SignalHub: () => SignalHub,
|
|
41382
|
+
createSignalStream: () => createSignalStream,
|
|
41383
|
+
getSignalHub: () => getSignalHub,
|
|
41384
|
+
setSignal: () => setSignal,
|
|
41385
|
+
sseFrameWithId: () => sseFrameWithId,
|
|
41386
|
+
writeSignalFromClient: () => writeSignalFromClient
|
|
41387
|
+
});
|
|
41014
41388
|
function getSignalHub() {
|
|
41015
41389
|
if (!hub) hub = new SignalHub();
|
|
41016
41390
|
return hub;
|
|
@@ -41032,11 +41406,158 @@ function setSignal(name, value, opts) {
|
|
|
41032
41406
|
}
|
|
41033
41407
|
return getSignalHub().setSignal(name, value, session ? { session } : void 0);
|
|
41034
41408
|
}
|
|
41035
|
-
|
|
41409
|
+
function sseFrameWithId(payload) {
|
|
41410
|
+
try {
|
|
41411
|
+
const seq = JSON.parse(payload).seq;
|
|
41412
|
+
if (typeof seq === "number") return "id: " + seq + "\ndata: " + payload + "\n\n";
|
|
41413
|
+
} catch {
|
|
41414
|
+
}
|
|
41415
|
+
return "data: " + payload + "\n\n";
|
|
41416
|
+
}
|
|
41417
|
+
function createSignalStream(request, url, opts) {
|
|
41418
|
+
const hub2 = getSignalHub();
|
|
41419
|
+
if (opts && !hub2.__cfg) {
|
|
41420
|
+
hub2.__cfg = true;
|
|
41421
|
+
try {
|
|
41422
|
+
hub2.configure({ maxClients: opts.maxClients, maxHistory: opts.maxHistory });
|
|
41423
|
+
} catch {
|
|
41424
|
+
}
|
|
41425
|
+
}
|
|
41426
|
+
if (url.searchParams.get("stats")) {
|
|
41427
|
+
return new Response(JSON.stringify(hub2.getStats(), null, 2), {
|
|
41428
|
+
status: 200,
|
|
41429
|
+
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }
|
|
41430
|
+
});
|
|
41431
|
+
}
|
|
41432
|
+
if (!hub2.canAcceptClient()) {
|
|
41433
|
+
return new Response("Too many signal-stream connections", {
|
|
41434
|
+
status: 503,
|
|
41435
|
+
headers: { "Content-Type": "text/plain", "Retry-After": "5" }
|
|
41436
|
+
});
|
|
41437
|
+
}
|
|
41438
|
+
const names = (url.searchParams.get("s") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
41439
|
+
let since = Number(url.searchParams.get("since") ?? "0") || 0;
|
|
41440
|
+
if (!since) {
|
|
41441
|
+
const lei = Number(request.headers.get("last-event-id") ?? "0") || 0;
|
|
41442
|
+
if (lei) since = lei;
|
|
41443
|
+
}
|
|
41444
|
+
const declared = /* @__PURE__ */ new Map();
|
|
41445
|
+
for (const n of names) declared.set(n, "public");
|
|
41446
|
+
let session;
|
|
41447
|
+
try {
|
|
41448
|
+
const header = request.headers.get("cookie") ?? "";
|
|
41449
|
+
const m = /(?:^|;\s*)tw_session=([^;]+)/.exec(header);
|
|
41450
|
+
if (m) session = decodeURIComponent(m[1].trim().replace(/^"|"$/g, ""));
|
|
41451
|
+
} catch {
|
|
41452
|
+
}
|
|
41453
|
+
const enc = new TextEncoder();
|
|
41454
|
+
const sse = (payload) => enc.encode(sseFrameWithId(payload));
|
|
41455
|
+
let registered = null;
|
|
41456
|
+
let keepAlive = null;
|
|
41457
|
+
const stream = new ReadableStream({
|
|
41458
|
+
start(controller) {
|
|
41459
|
+
const client = hub2.register(declared, session);
|
|
41460
|
+
registered = client;
|
|
41461
|
+
const cleanup = () => {
|
|
41462
|
+
if (keepAlive) clearInterval(keepAlive);
|
|
41463
|
+
hub2.unregister(client.id);
|
|
41464
|
+
try {
|
|
41465
|
+
controller.close();
|
|
41466
|
+
} catch {
|
|
41467
|
+
}
|
|
41468
|
+
};
|
|
41469
|
+
client.send = (payload) => {
|
|
41470
|
+
try {
|
|
41471
|
+
controller.enqueue(sse(payload));
|
|
41472
|
+
} catch {
|
|
41473
|
+
cleanup();
|
|
41474
|
+
}
|
|
41475
|
+
};
|
|
41476
|
+
client.close = cleanup;
|
|
41477
|
+
for (const payload of hub2.connectPayloads(since, declared, session)) {
|
|
41478
|
+
try {
|
|
41479
|
+
controller.enqueue(sse(payload));
|
|
41480
|
+
} catch {
|
|
41481
|
+
break;
|
|
41482
|
+
}
|
|
41483
|
+
}
|
|
41484
|
+
keepAlive = setInterval(() => {
|
|
41485
|
+
try {
|
|
41486
|
+
controller.enqueue(enc.encode(": keep-alive\n\n"));
|
|
41487
|
+
} catch {
|
|
41488
|
+
cleanup();
|
|
41489
|
+
}
|
|
41490
|
+
}, 25e3);
|
|
41491
|
+
},
|
|
41492
|
+
// v2: when the browser drops the connection (tab close, network
|
|
41493
|
+
// loss) cancel fires -- remove THIS hub entry immediately instead
|
|
41494
|
+
// of waiting for the next failed send.
|
|
41495
|
+
cancel() {
|
|
41496
|
+
if (keepAlive) clearInterval(keepAlive);
|
|
41497
|
+
try {
|
|
41498
|
+
registered?.close?.();
|
|
41499
|
+
} catch {
|
|
41500
|
+
}
|
|
41501
|
+
}
|
|
41502
|
+
});
|
|
41503
|
+
return new Response(stream, {
|
|
41504
|
+
status: 200,
|
|
41505
|
+
headers: {
|
|
41506
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
41507
|
+
"Cache-Control": "no-store",
|
|
41508
|
+
"X-Accel-Buffering": "no",
|
|
41509
|
+
Connection: "keep-alive"
|
|
41510
|
+
}
|
|
41511
|
+
});
|
|
41512
|
+
}
|
|
41513
|
+
async function writeSignalFromClient(request, opts) {
|
|
41514
|
+
if (!opts?.clientWrites) {
|
|
41515
|
+
return new Response(JSON.stringify({ error: "client signal writes are disabled" }), {
|
|
41516
|
+
status: 403,
|
|
41517
|
+
headers: { "Content-Type": "application/json" }
|
|
41518
|
+
});
|
|
41519
|
+
}
|
|
41520
|
+
let body = null;
|
|
41521
|
+
try {
|
|
41522
|
+
body = await request.json();
|
|
41523
|
+
} catch {
|
|
41524
|
+
}
|
|
41525
|
+
const name = body?.name;
|
|
41526
|
+
if (typeof name !== "string" || !("value" in (body ?? {}))) {
|
|
41527
|
+
return new Response(JSON.stringify({ error: "body must be { name, value }" }), {
|
|
41528
|
+
status: 400,
|
|
41529
|
+
headers: { "Content-Type": "application/json" }
|
|
41530
|
+
});
|
|
41531
|
+
}
|
|
41532
|
+
const hub2 = getSignalHub();
|
|
41533
|
+
const kind = (hub2.getStats().signals ?? {})[name];
|
|
41534
|
+
if (kind && kind !== "public") {
|
|
41535
|
+
return new Response(JSON.stringify({ error: "only public signals are client-writable" }), {
|
|
41536
|
+
status: 403,
|
|
41537
|
+
headers: { "Content-Type": "application/json" }
|
|
41538
|
+
});
|
|
41539
|
+
}
|
|
41540
|
+
const res = hub2.setSignal(name, body.value);
|
|
41541
|
+
if (!res.queued) {
|
|
41542
|
+
return new Response(JSON.stringify({ error: res.error ?? "rejected" }), {
|
|
41543
|
+
status: 400,
|
|
41544
|
+
headers: { "Content-Type": "application/json" }
|
|
41545
|
+
});
|
|
41546
|
+
}
|
|
41547
|
+
return new Response(JSON.stringify({ ok: true, kind: res.kind }), {
|
|
41548
|
+
status: 202,
|
|
41549
|
+
headers: { "Content-Type": "application/json" }
|
|
41550
|
+
});
|
|
41551
|
+
}
|
|
41552
|
+
var BATCH_MS, HISTORY_LIMIT, MAX_CLIENTS, MAX_VALUE_BYTES, MAX_NAME_LEN, NAME_RE, SignalHub, hub;
|
|
41036
41553
|
var init_signal_stream = __esm({
|
|
41037
41554
|
"../../packages/server/tw/routing/signal-stream.ts"() {
|
|
41038
41555
|
BATCH_MS = 100;
|
|
41039
41556
|
HISTORY_LIMIT = 1e3;
|
|
41557
|
+
MAX_CLIENTS = 500;
|
|
41558
|
+
MAX_VALUE_BYTES = 64 * 1024;
|
|
41559
|
+
MAX_NAME_LEN = 64;
|
|
41560
|
+
NAME_RE = /^[A-Za-z0-9_$.-]{1,64}$/;
|
|
41040
41561
|
SignalHub = class {
|
|
41041
41562
|
clients = /* @__PURE__ */ new Map();
|
|
41042
41563
|
nextClientId = 1;
|
|
@@ -41047,12 +41568,82 @@ var init_signal_stream = __esm({
|
|
|
41047
41568
|
pending = /* @__PURE__ */ new Map();
|
|
41048
41569
|
pendingSession;
|
|
41049
41570
|
timer = null;
|
|
41571
|
+
// v2 stats + caps
|
|
41572
|
+
maxClients = MAX_CLIENTS;
|
|
41573
|
+
createdAt = Date.now();
|
|
41574
|
+
totalUpdates = 0;
|
|
41575
|
+
batchesFlushed = 0;
|
|
41576
|
+
rejectedCount = 0;
|
|
41577
|
+
/** v2: configure caps (from tw.config.ts signalStream options). */
|
|
41578
|
+
configure(opts) {
|
|
41579
|
+
if (opts?.maxClients && opts.maxClients > 0) this.maxClients = opts.maxClients;
|
|
41580
|
+
if (opts?.maxHistory && opts.maxHistory > 0) this.historyLimit = opts.maxHistory;
|
|
41581
|
+
}
|
|
41582
|
+
historyLimit = HISTORY_LIMIT;
|
|
41583
|
+
/** v2: observability for /_tw/stream?stats=1 and window.__tw stats. */
|
|
41584
|
+
getStats() {
|
|
41585
|
+
const signals = {};
|
|
41586
|
+
for (const [n, k] of this.kinds) signals[n] = k;
|
|
41587
|
+
return {
|
|
41588
|
+
clients: this.clients.size,
|
|
41589
|
+
clientCap: this.maxClients,
|
|
41590
|
+
seq: this.seq,
|
|
41591
|
+
signals,
|
|
41592
|
+
valueCount: this.values.size,
|
|
41593
|
+
history: this.history.length,
|
|
41594
|
+
historyCap: this.historyLimit,
|
|
41595
|
+
pending: this.pending.size,
|
|
41596
|
+
totalUpdates: this.totalUpdates,
|
|
41597
|
+
batchesFlushed: this.batchesFlushed,
|
|
41598
|
+
rejected: this.rejectedCount,
|
|
41599
|
+
uptimeMs: Date.now() - this.createdAt
|
|
41600
|
+
};
|
|
41601
|
+
}
|
|
41602
|
+
/** v2: graceful shutdown -- close every connected stream now. */
|
|
41603
|
+
closeAll() {
|
|
41604
|
+
for (const c of [...this.clients.values()]) {
|
|
41605
|
+
try {
|
|
41606
|
+
c.close();
|
|
41607
|
+
} catch {
|
|
41608
|
+
}
|
|
41609
|
+
}
|
|
41610
|
+
this.clients.clear();
|
|
41611
|
+
if (this.timer != null) {
|
|
41612
|
+
clearTimeout(this.timer);
|
|
41613
|
+
this.timer = null;
|
|
41614
|
+
}
|
|
41615
|
+
this.pending.clear();
|
|
41616
|
+
}
|
|
41617
|
+
/** v2: connection cap check BEFORE registering a stream client. */
|
|
41618
|
+
canAcceptClient() {
|
|
41619
|
+
return this.clients.size < this.maxClients;
|
|
41620
|
+
}
|
|
41621
|
+
/** v2: validate a setSignal name/value without throwing. */
|
|
41622
|
+
validateSignal(name, value) {
|
|
41623
|
+
if (typeof name !== "string" || name.length === 0 || name.length > MAX_NAME_LEN) {
|
|
41624
|
+
return "signal name must be 1-" + MAX_NAME_LEN + " chars";
|
|
41625
|
+
}
|
|
41626
|
+
if (!NAME_RE.test(name)) return "signal name has invalid characters";
|
|
41627
|
+
if (value === void 0) return "signal value is undefined";
|
|
41628
|
+
try {
|
|
41629
|
+
const size = typeof value === "string" ? value.length : JSON.stringify(value)?.length ?? 0;
|
|
41630
|
+
if (size > MAX_VALUE_BYTES) return "signal value exceeds " + MAX_VALUE_BYTES + " bytes";
|
|
41631
|
+
} catch {
|
|
41632
|
+
return "signal value is not serializable";
|
|
41633
|
+
}
|
|
41634
|
+
return null;
|
|
41635
|
+
}
|
|
41050
41636
|
/**
|
|
41051
41637
|
* Server-side mutation (route handlers): queue an update, batch-flush.
|
|
41052
41638
|
* With `session` (derived from the request's tw_session cookie) private
|
|
41053
41639
|
* signals are delivered only to clients authenticated as that session.
|
|
41054
41640
|
*/
|
|
41055
41641
|
setSignal(name, value, opts) {
|
|
41642
|
+
const invalid = this.validateSignal(name, value);
|
|
41643
|
+
if (invalid) {
|
|
41644
|
+
this.rejectedCount++;
|
|
41645
|
+
return { queued: false, kind: this.kinds.get(name), error: invalid };
|
|
41646
|
+
}
|
|
41056
41647
|
const kind = this.kinds.get(name) ?? "public";
|
|
41057
41648
|
this.values.set(name, value);
|
|
41058
41649
|
this.kinds.set(name, kind);
|
|
@@ -41083,9 +41674,27 @@ var init_signal_stream = __esm({
|
|
|
41083
41674
|
this.clients.set(id, client);
|
|
41084
41675
|
return client;
|
|
41085
41676
|
}
|
|
41677
|
+
/** v2: remove a stream client (its connection died / was cancelled).
|
|
41678
|
+
* The old handler overwrote client.close() with a closure that only
|
|
41679
|
+
* closed the controller -- the Map entry leaked forever, so every
|
|
41680
|
+
* disconnected browser still occupied a slot (and got flushed frames).
|
|
41681
|
+
*/
|
|
41682
|
+
unregister(id) {
|
|
41683
|
+
this.clients.delete(id);
|
|
41684
|
+
}
|
|
41086
41685
|
/** Build the replay/snapshot preamble for a connecting client. */
|
|
41087
41686
|
connectPayloads(since, declared, session) {
|
|
41088
41687
|
const out = [];
|
|
41688
|
+
if (since <= 0 && this.values.size > 0) {
|
|
41689
|
+
const snap = {};
|
|
41690
|
+
for (const [name, value] of this.values) {
|
|
41691
|
+
if (this.sessionMatches(session, { names: [name], session: this.valueSessions.get(name) }) && this.deliversTo(declared, [name])) snap[name] = value;
|
|
41692
|
+
}
|
|
41693
|
+
if (Object.keys(snap).length > 0) {
|
|
41694
|
+
out.push(this.frame({ v: 1, seq: this.seq, snapshot: snap }));
|
|
41695
|
+
}
|
|
41696
|
+
return out;
|
|
41697
|
+
}
|
|
41089
41698
|
const hasCoverage = since <= this.seq && this.history.length > 0 && since >= this.history[0].seq - 1;
|
|
41090
41699
|
if (hasCoverage) {
|
|
41091
41700
|
for (const entry of this.history) {
|
|
@@ -41128,7 +41737,9 @@ var init_signal_stream = __esm({
|
|
|
41128
41737
|
for (const [n] of updates) this.valueSessions.set(n, session);
|
|
41129
41738
|
const payload = this.frame({ v: 1, seq: this.seq, updates });
|
|
41130
41739
|
this.history.push({ seq: this.seq, payload, names, session });
|
|
41131
|
-
|
|
41740
|
+
this.totalUpdates += updates.length;
|
|
41741
|
+
this.batchesFlushed++;
|
|
41742
|
+
if (this.history.length > this.historyLimit) this.history.shift();
|
|
41132
41743
|
for (const client of this.clients.values()) {
|
|
41133
41744
|
if (this.sessionMatches(client.session, { names, session }) && this.deliversTo(client.declared, names)) client.send(payload);
|
|
41134
41745
|
}
|
|
@@ -41815,6 +42426,7 @@ var init_render_pipeline = __esm({
|
|
|
41815
42426
|
init_twm_loader();
|
|
41816
42427
|
init_scanner();
|
|
41817
42428
|
init_layout_chain();
|
|
42429
|
+
init_signal_stream();
|
|
41818
42430
|
init_helpers();
|
|
41819
42431
|
init_tw6();
|
|
41820
42432
|
init_twm_loader();
|
|
@@ -41968,6 +42580,17 @@ var init_render_pipeline = __esm({
|
|
|
41968
42580
|
try {
|
|
41969
42581
|
const pageState = { ...stateVars ?? {}, ...match.params ?? {}, params: match.params ?? {} };
|
|
41970
42582
|
const requestVarKeys = new Set(Object.keys({ ...stateVars ?? {}, ...match.params ?? {} }));
|
|
42583
|
+
try {
|
|
42584
|
+
const hub2 = getSignalHub();
|
|
42585
|
+
const snap = hub2.snapshot();
|
|
42586
|
+
if (snap && snap.snapshot && typeof snap.snapshot === "object") {
|
|
42587
|
+
for (const [k, v] of Object.entries(snap.snapshot)) {
|
|
42588
|
+
pageState[k] = v;
|
|
42589
|
+
requestVarKeys.add(k);
|
|
42590
|
+
}
|
|
42591
|
+
}
|
|
42592
|
+
} catch {
|
|
42593
|
+
}
|
|
41971
42594
|
const pageCompiled = this.compileFile(ctx.page, pageState);
|
|
41972
42595
|
{
|
|
41973
42596
|
const seed = pageCompiled.stateSeed;
|
|
@@ -42013,24 +42636,30 @@ var init_render_pipeline = __esm({
|
|
|
42013
42636
|
let layoutTitle;
|
|
42014
42637
|
const liftedParts = [];
|
|
42015
42638
|
{
|
|
42016
|
-
|
|
42639
|
+
const collectedTitles = [];
|
|
42640
|
+
for (const headMatch of bodyHTML.matchAll(/<head(?:\s[^>]*)?>([\s\S]*?)<\/head\s*>/gi)) {
|
|
42017
42641
|
const headInner = headMatch[1];
|
|
42018
42642
|
const tMatch = headInner.match(/<title>([^<]*)<\/title>/i);
|
|
42019
|
-
if (tMatch)
|
|
42020
|
-
|
|
42643
|
+
if (tMatch) collectedTitles.push(tMatch[1].trim().replace(/&(amp|lt|gt|quot);/g, (m, k) => k === "amp" ? String.fromCharCode(38) : k === "lt" ? String.fromCharCode(60) : k === "gt" ? String.fromCharCode(62) : String.fromCharCode(34)));
|
|
42644
|
+
else collectedTitles.push("");
|
|
42645
|
+
const kids = headInner.replace(/<title>[^<]*<\/title>/gi, "").replace(/<meta[^>]+charset[^>]*>/gi, "").replace(/<meta[^>]+viewport[^>]*>/gi, "").replace(/<meta[^>]+generator[^>]*>/gi, "").trim();
|
|
42021
42646
|
if (kids) liftedParts.push(kids);
|
|
42022
42647
|
}
|
|
42023
|
-
bodyHTML = bodyHTML.replace(/<head[^>]
|
|
42648
|
+
bodyHTML = bodyHTML.replace(/<head(?:\s[^>]*)?>[\s\S]*?<\/head\s*>/gi, "");
|
|
42649
|
+
const templateTitle = collectedTitles.find((t2) => t2.includes("__TW_PAGETITLE__"));
|
|
42650
|
+
layoutTitle = templateTitle ?? collectedTitles.filter(Boolean).pop();
|
|
42024
42651
|
bodyHTML = bodyHTML.replace(/<\/?(?:html|body)[^>]*>/gi, "");
|
|
42025
42652
|
bodyHTML = bodyHTML.replace(/<!DOCTYPE[^>]*>/gi, "");
|
|
42026
42653
|
}
|
|
42027
42654
|
const liftedHead = liftedParts.join("\n");
|
|
42028
|
-
|
|
42655
|
+
const liveRenderMode = this.extractRenderMode(ctx.page);
|
|
42656
|
+
if (ctx.error && (liveRenderMode === "ssr" || liveRenderMode === "stream")) {
|
|
42029
42657
|
const errorCompiled = this.compileFile(ctx.error);
|
|
42030
42658
|
allCSS = errorCompiled.css + "\n" + allCSS;
|
|
42031
42659
|
bodyHTML = `<div data-error-boundary>${bodyHTML}</div>`;
|
|
42032
42660
|
}
|
|
42033
|
-
|
|
42661
|
+
const needsSkeleton = bodyHTML.includes("data-tw-suspense") || bodyHTML.includes("data-tw-ppr");
|
|
42662
|
+
if (ctx.loading && needsSkeleton) {
|
|
42034
42663
|
const loadingCompiled = this.compileFile(ctx.loading);
|
|
42035
42664
|
allCSS = loadingCompiled.css + "\n" + allCSS;
|
|
42036
42665
|
let lh = String(loadingCompiled.html || "");
|
|
@@ -42042,7 +42671,7 @@ ${bodyHTML}`;
|
|
|
42042
42671
|
const pageTitle = this.extractTitle(ctx.page) || "TW Page";
|
|
42043
42672
|
let title = pageTitle;
|
|
42044
42673
|
if (layoutTitle !== void 0) {
|
|
42045
|
-
title = layoutTitle.replace(/\{\s*page\.title\s*\}/g, pageTitle);
|
|
42674
|
+
title = layoutTitle.replace(/__TW_PAGETITLE__/g, pageTitle).replace(/\{\s*page\.title\s*\}/g, pageTitle);
|
|
42046
42675
|
if (!title.trim()) title = pageTitle;
|
|
42047
42676
|
}
|
|
42048
42677
|
const metaTags = this.buildMetaTags(ctx.page);
|
|
@@ -42062,7 +42691,9 @@ ${bodyHTML}`;
|
|
|
42062
42691
|
} catch {
|
|
42063
42692
|
}
|
|
42064
42693
|
let fullHTML = this.assembleHTML(bodyHTML, allCSS, allJS, title, metaTags + headExtra);
|
|
42065
|
-
|
|
42694
|
+
const pageRenderMode = this.extractRenderMode(ctx.page);
|
|
42695
|
+
const needsClientRuntime = /data-tw-event|data-tw-i|__TW_SIGNALS__|data-tw-suspense|data-tw-ppr/.test(fullHTML) || pageRenderMode === "csr" || pageRenderMode === "stream" || pageRenderMode === "ppr" || pageRenderMode === "signalStream";
|
|
42696
|
+
if (needsClientRuntime && !/__tw_runtime\.js/.test(fullHTML)) {
|
|
42066
42697
|
let stateSeed = "{}";
|
|
42067
42698
|
try {
|
|
42068
42699
|
stateSeed = JSON.stringify(pageState ?? {});
|
|
@@ -42181,7 +42812,8 @@ ${bodyHTML}`;
|
|
|
42181
42812
|
throw new Error(`Route file not found: ${file.absolutePath}`);
|
|
42182
42813
|
}
|
|
42183
42814
|
const source = readFileSync7(file.absolutePath, "utf-8");
|
|
42184
|
-
const
|
|
42815
|
+
const srcWithSentinel = source.replace(/\{\s*page\.title\s*\}/g, "__TW_PAGETITLE__");
|
|
42816
|
+
const result = compileSync(srcWithSentinel, {
|
|
42185
42817
|
filePath: file.absolutePath,
|
|
42186
42818
|
optimize: true,
|
|
42187
42819
|
diagnostics: false,
|
|
@@ -42255,12 +42887,13 @@ ${bodyHTML}`;
|
|
|
42255
42887
|
try {
|
|
42256
42888
|
const source = readFileSync7(file.absolutePath, "utf-8");
|
|
42257
42889
|
const masked = maskSourceStringsAndComments(source);
|
|
42258
|
-
const m = masked.match(/page\s*\{[^}]*title\s
|
|
42890
|
+
const m = masked.match(/page\s*\{[^}]*title\s/);
|
|
42259
42891
|
if (!m || m.index === void 0) return null;
|
|
42260
42892
|
const start = m.index + m[0].length;
|
|
42261
|
-
|
|
42262
|
-
|
|
42263
|
-
|
|
42893
|
+
if (source[start] !== '"') return null;
|
|
42894
|
+
const end = source.indexOf('"', start + 1);
|
|
42895
|
+
if (end === -1 || end === start + 1) return null;
|
|
42896
|
+
return source.slice(start + 1, end).replace(/\\([{}"])/g, "$1");
|
|
42264
42897
|
} catch {
|
|
42265
42898
|
return null;
|
|
42266
42899
|
}
|
|
@@ -42309,7 +42942,8 @@ ${bodyHTML}`;
|
|
|
42309
42942
|
* render-cache never reads `stale`.
|
|
42310
42943
|
*/
|
|
42311
42944
|
stampCacheHeaders(headers, entry, now, verdict) {
|
|
42312
|
-
const out = { ...headers ?? {}
|
|
42945
|
+
const out = { ...headers ?? {} };
|
|
42946
|
+
if (process.env.TW_DEBUG_CACHE || process.env.NODE_ENV === "test") out["x-tw-cache"] = verdict;
|
|
42313
42947
|
const c = entry?.cache;
|
|
42314
42948
|
if (c) {
|
|
42315
42949
|
const ageSec = entry?.createdAt ? Math.max(0, Math.floor((now - entry.createdAt) / 1e3)) : 0;
|
|
@@ -43843,7 +44477,6 @@ function securityHeadersMiddleware() {
|
|
|
43843
44477
|
return (req, res, next) => {
|
|
43844
44478
|
res.headers.set("X-Content-Type-Options", "nosniff");
|
|
43845
44479
|
res.headers.set("X-Frame-Options", "SAMEORIGIN");
|
|
43846
|
-
res.headers.set("X-XSS-Protection", "1; mode=block");
|
|
43847
44480
|
res.headers.set("X-Permitted-Cross-Domain-Policies", "none");
|
|
43848
44481
|
res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
43849
44482
|
res.headers.set("Cross-Origin-Opener-Policy", "same-origin");
|
|
@@ -44020,13 +44653,15 @@ function compressionMiddleware() {
|
|
|
44020
44653
|
await next();
|
|
44021
44654
|
};
|
|
44022
44655
|
}
|
|
44023
|
-
function securityHeadersMiddleware2() {
|
|
44656
|
+
function securityHeadersMiddleware2(opts) {
|
|
44024
44657
|
return async (ctx, next) => {
|
|
44025
44658
|
ctx.headers["x-content-type-options"] = "nosniff";
|
|
44026
44659
|
ctx.headers["x-frame-options"] = "DENY";
|
|
44027
|
-
ctx.headers["x-xss-protection"] = "1; mode=block";
|
|
44028
44660
|
ctx.headers["referrer-policy"] = "strict-origin-when-cross-origin";
|
|
44029
44661
|
ctx.headers["permissions-policy"] = "camera=(), microphone=(), geolocation=()";
|
|
44662
|
+
if (opts?.csp !== false && !ctx.headers["content-security-policy"]) {
|
|
44663
|
+
ctx.headers["content-security-policy"] = typeof opts?.csp === "string" ? opts.csp : "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'";
|
|
44664
|
+
}
|
|
44030
44665
|
await next();
|
|
44031
44666
|
};
|
|
44032
44667
|
}
|
|
@@ -44109,6 +44744,161 @@ var init_middleware2 = __esm({
|
|
|
44109
44744
|
}
|
|
44110
44745
|
});
|
|
44111
44746
|
|
|
44747
|
+
// ../../packages/server/tw/compression.ts
|
|
44748
|
+
function shouldCompress(contentType, options = {}) {
|
|
44749
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44750
|
+
const ct = contentType.split(";")[0].trim();
|
|
44751
|
+
return opts.contentTypes.includes(ct);
|
|
44752
|
+
}
|
|
44753
|
+
function isLargeEnough(size, options = {}) {
|
|
44754
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44755
|
+
return size >= opts.threshold;
|
|
44756
|
+
}
|
|
44757
|
+
async function compress(data, encoding = "gzip", options = {}) {
|
|
44758
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44759
|
+
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
44760
|
+
if (input.length < opts.threshold) {
|
|
44761
|
+
return input;
|
|
44762
|
+
}
|
|
44763
|
+
try {
|
|
44764
|
+
if (typeof Bun !== "undefined" && Bun.gzipSync) {
|
|
44765
|
+
if (encoding === "gzip") return Bun.gzipSync(input, { level: opts.level });
|
|
44766
|
+
if (encoding === "deflate" && Bun.deflateSync) return Bun.deflateSync(input, { level: opts.level });
|
|
44767
|
+
}
|
|
44768
|
+
} catch {
|
|
44769
|
+
}
|
|
44770
|
+
try {
|
|
44771
|
+
const zlib2 = await import("node:zlib");
|
|
44772
|
+
if (encoding === "gzip") {
|
|
44773
|
+
return zlib2.gzipSync(input, { level: opts.level });
|
|
44774
|
+
}
|
|
44775
|
+
if (encoding === "deflate") {
|
|
44776
|
+
return zlib2.deflateSync(input, { level: opts.level });
|
|
44777
|
+
}
|
|
44778
|
+
if (encoding === "brotli" && zlib2.brotliCompressSync) {
|
|
44779
|
+
return zlib2.brotliCompressSync(input, {
|
|
44780
|
+
params: {
|
|
44781
|
+
[zlib2.constants.BROTLI_PARAM_QUALITY]: opts.level
|
|
44782
|
+
}
|
|
44783
|
+
});
|
|
44784
|
+
}
|
|
44785
|
+
} catch {
|
|
44786
|
+
}
|
|
44787
|
+
return input;
|
|
44788
|
+
}
|
|
44789
|
+
async function decompress(data, encoding = "gzip") {
|
|
44790
|
+
try {
|
|
44791
|
+
if (typeof Bun !== "undefined" && Bun.gunzipSync) {
|
|
44792
|
+
if (encoding === "gzip") return Bun.gunzipSync(data);
|
|
44793
|
+
if (encoding === "deflate" && Bun.inflateSync) return Bun.inflateSync(data);
|
|
44794
|
+
}
|
|
44795
|
+
} catch {
|
|
44796
|
+
}
|
|
44797
|
+
try {
|
|
44798
|
+
const zlib2 = await import("node:zlib");
|
|
44799
|
+
if (encoding === "gzip") return zlib2.gunzipSync(data);
|
|
44800
|
+
if (encoding === "deflate") return zlib2.inflateSync(data);
|
|
44801
|
+
if (encoding === "brotli" && zlib2.brotliDecompressSync) return zlib2.brotliDecompressSync(data);
|
|
44802
|
+
} catch {
|
|
44803
|
+
}
|
|
44804
|
+
return data;
|
|
44805
|
+
}
|
|
44806
|
+
function negotiateEncoding(acceptEncoding) {
|
|
44807
|
+
const encodings = acceptEncoding.split(",").map((e) => e.trim().split(";")[0].trim()).filter(Boolean);
|
|
44808
|
+
if (encodings.includes("br")) return "brotli";
|
|
44809
|
+
if (encodings.includes("gzip")) return "gzip";
|
|
44810
|
+
if (encodings.includes("deflate")) return "deflate";
|
|
44811
|
+
return null;
|
|
44812
|
+
}
|
|
44813
|
+
function createCompressionHeaders(encoding, originalSize, compressedSize) {
|
|
44814
|
+
return {
|
|
44815
|
+
"Content-Encoding": encoding === "brotli" ? "br" : encoding,
|
|
44816
|
+
"Vary": "Accept-Encoding",
|
|
44817
|
+
"X-Original-Size": String(originalSize),
|
|
44818
|
+
"X-Compressed-Size": String(compressedSize),
|
|
44819
|
+
"X-Compression-Ratio": originalSize > 0 ? `${((1 - compressedSize / originalSize) * 100).toFixed(1)}%` : "0%"
|
|
44820
|
+
};
|
|
44821
|
+
}
|
|
44822
|
+
async function compressResponse(body, contentType, acceptEncoding, options = {}) {
|
|
44823
|
+
const input = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
44824
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
44825
|
+
if (!shouldCompress(contentType, opts) || !isLargeEnough(input.length, opts)) {
|
|
44826
|
+
return { body: input, headers: {}, compressed: false };
|
|
44827
|
+
}
|
|
44828
|
+
const encoding = negotiateEncoding(acceptEncoding);
|
|
44829
|
+
if (!encoding) {
|
|
44830
|
+
return { body: input, headers: {}, compressed: false };
|
|
44831
|
+
}
|
|
44832
|
+
try {
|
|
44833
|
+
const compressed = await compress(input, encoding, opts);
|
|
44834
|
+
const headers = createCompressionHeaders(encoding, input.length, compressed.length);
|
|
44835
|
+
return { body: compressed, headers, compressed: true };
|
|
44836
|
+
} catch {
|
|
44837
|
+
return { body: input, headers: {}, compressed: false };
|
|
44838
|
+
}
|
|
44839
|
+
}
|
|
44840
|
+
var DEFAULT_OPTIONS4, StreamingCompressor;
|
|
44841
|
+
var init_compression = __esm({
|
|
44842
|
+
"../../packages/server/tw/compression.ts"() {
|
|
44843
|
+
DEFAULT_OPTIONS4 = {
|
|
44844
|
+
level: 6,
|
|
44845
|
+
threshold: 1024,
|
|
44846
|
+
// Don't compress < 1KB
|
|
44847
|
+
brotli: true,
|
|
44848
|
+
contentTypes: [
|
|
44849
|
+
"text/html",
|
|
44850
|
+
"text/css",
|
|
44851
|
+
"text/plain",
|
|
44852
|
+
"text/javascript",
|
|
44853
|
+
"application/javascript",
|
|
44854
|
+
"application/json",
|
|
44855
|
+
"application/xml",
|
|
44856
|
+
"application/xhtml+xml",
|
|
44857
|
+
"image/svg+xml",
|
|
44858
|
+
"font/woff",
|
|
44859
|
+
"font/woff2",
|
|
44860
|
+
"application/wasm"
|
|
44861
|
+
]
|
|
44862
|
+
};
|
|
44863
|
+
StreamingCompressor = class {
|
|
44864
|
+
chunks = [];
|
|
44865
|
+
encoding;
|
|
44866
|
+
options;
|
|
44867
|
+
constructor(encoding = "gzip", options = {}) {
|
|
44868
|
+
this.encoding = encoding;
|
|
44869
|
+
this.options = { ...DEFAULT_OPTIONS4, ...options };
|
|
44870
|
+
}
|
|
44871
|
+
/**
|
|
44872
|
+
* Add a chunk of data to be compressed.
|
|
44873
|
+
*/
|
|
44874
|
+
push(data) {
|
|
44875
|
+
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
44876
|
+
this.chunks.push(input);
|
|
44877
|
+
}
|
|
44878
|
+
/**
|
|
44879
|
+
* Flush all data and return compressed result.
|
|
44880
|
+
*/
|
|
44881
|
+
async flush() {
|
|
44882
|
+
const totalLength = this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
44883
|
+
const combined = new Uint8Array(totalLength);
|
|
44884
|
+
let offset = 0;
|
|
44885
|
+
for (const chunk of this.chunks) {
|
|
44886
|
+
combined.set(chunk, offset);
|
|
44887
|
+
offset += chunk.length;
|
|
44888
|
+
}
|
|
44889
|
+
this.chunks = [];
|
|
44890
|
+
return compress(combined, this.encoding, this.options);
|
|
44891
|
+
}
|
|
44892
|
+
/**
|
|
44893
|
+
* Get the total uncompressed size so far.
|
|
44894
|
+
*/
|
|
44895
|
+
get uncompressedSize() {
|
|
44896
|
+
return this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
44897
|
+
}
|
|
44898
|
+
};
|
|
44899
|
+
}
|
|
44900
|
+
});
|
|
44901
|
+
|
|
44112
44902
|
// ../../packages/server/tw/static/index.ts
|
|
44113
44903
|
import { existsSync as existsSync11, statSync as statSync6 } from "node:fs";
|
|
44114
44904
|
import { join as join15, extname as extname7, normalize as normalize2, resolve as resolve4, sep } from "node:path";
|
|
@@ -44318,7 +45108,7 @@ var init_policy = __esm({
|
|
|
44318
45108
|
"Content-Security-Policy": this.build(),
|
|
44319
45109
|
"X-Content-Type-Options": "nosniff",
|
|
44320
45110
|
"X-Frame-Options": "DENY",
|
|
44321
|
-
|
|
45111
|
+
// BUG 48: x-xss-protection removed (deprecated, harmful in old browsers)
|
|
44322
45112
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
44323
45113
|
"Permissions-Policy": "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()",
|
|
44324
45114
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
@@ -48952,9 +49742,10 @@ function devSecurityHeaders() {
|
|
|
48952
49742
|
removePoweredBy: true
|
|
48953
49743
|
});
|
|
48954
49744
|
}
|
|
48955
|
-
var DEFAULTS2, SecurityHeaders2;
|
|
49745
|
+
var BASELINE_CSP, DEFAULTS2, SecurityHeaders2;
|
|
48956
49746
|
var init_security_headers = __esm({
|
|
48957
49747
|
"../../packages/security/tw/headers/security-headers.ts"() {
|
|
49748
|
+
BASELINE_CSP = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'";
|
|
48958
49749
|
DEFAULTS2 = {
|
|
48959
49750
|
hstsMaxAge: 31536e3,
|
|
48960
49751
|
// 1 year
|
|
@@ -48963,7 +49754,9 @@ var init_security_headers = __esm({
|
|
|
48963
49754
|
frameOptions: "SAMEORIGIN",
|
|
48964
49755
|
contentTypeOptions: true,
|
|
48965
49756
|
referrerPolicy: "strict-origin-when-cross-origin",
|
|
48966
|
-
|
|
49757
|
+
// v1.0.8 round 5 (BUG 48): x-xss-protection deprecated since 2019 --
|
|
49758
|
+
// OFF by default now; opt back in with security.xssProtection.
|
|
49759
|
+
xssProtection: void 0,
|
|
48967
49760
|
removePoweredBy: true,
|
|
48968
49761
|
coep: "unsafe-none",
|
|
48969
49762
|
coop: "same-origin",
|
|
@@ -49019,6 +49812,9 @@ var init_security_headers = __esm({
|
|
|
49019
49812
|
if (this.config.serverHeader) {
|
|
49020
49813
|
headers["Server"] = this.config.serverHeader;
|
|
49021
49814
|
}
|
|
49815
|
+
if (this.config.csp !== false && !headers["Content-Security-Policy"]) {
|
|
49816
|
+
headers["Content-Security-Policy"] = typeof this.config.csp === "string" ? this.config.csp : BASELINE_CSP;
|
|
49817
|
+
}
|
|
49022
49818
|
return headers;
|
|
49023
49819
|
}
|
|
49024
49820
|
/** Returns the security headers object. */
|
|
@@ -53229,161 +54025,6 @@ var init_ssr = __esm({
|
|
|
53229
54025
|
}
|
|
53230
54026
|
});
|
|
53231
54027
|
|
|
53232
|
-
// ../../packages/server/tw/compression.ts
|
|
53233
|
-
function shouldCompress(contentType, options = {}) {
|
|
53234
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53235
|
-
const ct = contentType.split(";")[0].trim();
|
|
53236
|
-
return opts.contentTypes.includes(ct);
|
|
53237
|
-
}
|
|
53238
|
-
function isLargeEnough(size, options = {}) {
|
|
53239
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53240
|
-
return size >= opts.threshold;
|
|
53241
|
-
}
|
|
53242
|
-
async function compress(data, encoding = "gzip", options = {}) {
|
|
53243
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53244
|
-
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
53245
|
-
if (input.length < opts.threshold) {
|
|
53246
|
-
return input;
|
|
53247
|
-
}
|
|
53248
|
-
try {
|
|
53249
|
-
if (typeof Bun !== "undefined" && Bun.gzipSync) {
|
|
53250
|
-
if (encoding === "gzip") return Bun.gzipSync(input, { level: opts.level });
|
|
53251
|
-
if (encoding === "deflate" && Bun.deflateSync) return Bun.deflateSync(input, { level: opts.level });
|
|
53252
|
-
}
|
|
53253
|
-
} catch {
|
|
53254
|
-
}
|
|
53255
|
-
try {
|
|
53256
|
-
const zlib2 = await import("node:zlib");
|
|
53257
|
-
if (encoding === "gzip") {
|
|
53258
|
-
return zlib2.gzipSync(input, { level: opts.level });
|
|
53259
|
-
}
|
|
53260
|
-
if (encoding === "deflate") {
|
|
53261
|
-
return zlib2.deflateSync(input, { level: opts.level });
|
|
53262
|
-
}
|
|
53263
|
-
if (encoding === "brotli" && zlib2.brotliCompressSync) {
|
|
53264
|
-
return zlib2.brotliCompressSync(input, {
|
|
53265
|
-
params: {
|
|
53266
|
-
[zlib2.constants.BROTLI_PARAM_QUALITY]: opts.level
|
|
53267
|
-
}
|
|
53268
|
-
});
|
|
53269
|
-
}
|
|
53270
|
-
} catch {
|
|
53271
|
-
}
|
|
53272
|
-
return input;
|
|
53273
|
-
}
|
|
53274
|
-
async function decompress(data, encoding = "gzip") {
|
|
53275
|
-
try {
|
|
53276
|
-
if (typeof Bun !== "undefined" && Bun.gunzipSync) {
|
|
53277
|
-
if (encoding === "gzip") return Bun.gunzipSync(data);
|
|
53278
|
-
if (encoding === "deflate" && Bun.inflateSync) return Bun.inflateSync(data);
|
|
53279
|
-
}
|
|
53280
|
-
} catch {
|
|
53281
|
-
}
|
|
53282
|
-
try {
|
|
53283
|
-
const zlib2 = await import("node:zlib");
|
|
53284
|
-
if (encoding === "gzip") return zlib2.gunzipSync(data);
|
|
53285
|
-
if (encoding === "deflate") return zlib2.inflateSync(data);
|
|
53286
|
-
if (encoding === "brotli" && zlib2.brotliDecompressSync) return zlib2.brotliDecompressSync(data);
|
|
53287
|
-
} catch {
|
|
53288
|
-
}
|
|
53289
|
-
return data;
|
|
53290
|
-
}
|
|
53291
|
-
function negotiateEncoding(acceptEncoding) {
|
|
53292
|
-
const encodings = acceptEncoding.split(",").map((e) => e.trim().split(";")[0].trim()).filter(Boolean);
|
|
53293
|
-
if (encodings.includes("br")) return "brotli";
|
|
53294
|
-
if (encodings.includes("gzip")) return "gzip";
|
|
53295
|
-
if (encodings.includes("deflate")) return "deflate";
|
|
53296
|
-
return null;
|
|
53297
|
-
}
|
|
53298
|
-
function createCompressionHeaders(encoding, originalSize, compressedSize) {
|
|
53299
|
-
return {
|
|
53300
|
-
"Content-Encoding": encoding === "brotli" ? "br" : encoding,
|
|
53301
|
-
"Vary": "Accept-Encoding",
|
|
53302
|
-
"X-Original-Size": String(originalSize),
|
|
53303
|
-
"X-Compressed-Size": String(compressedSize),
|
|
53304
|
-
"X-Compression-Ratio": originalSize > 0 ? `${((1 - compressedSize / originalSize) * 100).toFixed(1)}%` : "0%"
|
|
53305
|
-
};
|
|
53306
|
-
}
|
|
53307
|
-
async function compressResponse(body, contentType, acceptEncoding, options = {}) {
|
|
53308
|
-
const input = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
53309
|
-
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
53310
|
-
if (!shouldCompress(contentType, opts) || !isLargeEnough(input.length, opts)) {
|
|
53311
|
-
return { body: input, headers: {}, compressed: false };
|
|
53312
|
-
}
|
|
53313
|
-
const encoding = negotiateEncoding(acceptEncoding);
|
|
53314
|
-
if (!encoding) {
|
|
53315
|
-
return { body: input, headers: {}, compressed: false };
|
|
53316
|
-
}
|
|
53317
|
-
try {
|
|
53318
|
-
const compressed = await compress(input, encoding, opts);
|
|
53319
|
-
const headers = createCompressionHeaders(encoding, input.length, compressed.length);
|
|
53320
|
-
return { body: compressed, headers, compressed: true };
|
|
53321
|
-
} catch {
|
|
53322
|
-
return { body: input, headers: {}, compressed: false };
|
|
53323
|
-
}
|
|
53324
|
-
}
|
|
53325
|
-
var DEFAULT_OPTIONS4, StreamingCompressor;
|
|
53326
|
-
var init_compression = __esm({
|
|
53327
|
-
"../../packages/server/tw/compression.ts"() {
|
|
53328
|
-
DEFAULT_OPTIONS4 = {
|
|
53329
|
-
level: 6,
|
|
53330
|
-
threshold: 1024,
|
|
53331
|
-
// Don't compress < 1KB
|
|
53332
|
-
brotli: true,
|
|
53333
|
-
contentTypes: [
|
|
53334
|
-
"text/html",
|
|
53335
|
-
"text/css",
|
|
53336
|
-
"text/plain",
|
|
53337
|
-
"text/javascript",
|
|
53338
|
-
"application/javascript",
|
|
53339
|
-
"application/json",
|
|
53340
|
-
"application/xml",
|
|
53341
|
-
"application/xhtml+xml",
|
|
53342
|
-
"image/svg+xml",
|
|
53343
|
-
"font/woff",
|
|
53344
|
-
"font/woff2",
|
|
53345
|
-
"application/wasm"
|
|
53346
|
-
]
|
|
53347
|
-
};
|
|
53348
|
-
StreamingCompressor = class {
|
|
53349
|
-
chunks = [];
|
|
53350
|
-
encoding;
|
|
53351
|
-
options;
|
|
53352
|
-
constructor(encoding = "gzip", options = {}) {
|
|
53353
|
-
this.encoding = encoding;
|
|
53354
|
-
this.options = { ...DEFAULT_OPTIONS4, ...options };
|
|
53355
|
-
}
|
|
53356
|
-
/**
|
|
53357
|
-
* Add a chunk of data to be compressed.
|
|
53358
|
-
*/
|
|
53359
|
-
push(data) {
|
|
53360
|
-
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
53361
|
-
this.chunks.push(input);
|
|
53362
|
-
}
|
|
53363
|
-
/**
|
|
53364
|
-
* Flush all data and return compressed result.
|
|
53365
|
-
*/
|
|
53366
|
-
async flush() {
|
|
53367
|
-
const totalLength = this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
53368
|
-
const combined = new Uint8Array(totalLength);
|
|
53369
|
-
let offset = 0;
|
|
53370
|
-
for (const chunk of this.chunks) {
|
|
53371
|
-
combined.set(chunk, offset);
|
|
53372
|
-
offset += chunk.length;
|
|
53373
|
-
}
|
|
53374
|
-
this.chunks = [];
|
|
53375
|
-
return compress(combined, this.encoding, this.options);
|
|
53376
|
-
}
|
|
53377
|
-
/**
|
|
53378
|
-
* Get the total uncompressed size so far.
|
|
53379
|
-
*/
|
|
53380
|
-
get uncompressedSize() {
|
|
53381
|
-
return this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
53382
|
-
}
|
|
53383
|
-
};
|
|
53384
|
-
}
|
|
53385
|
-
});
|
|
53386
|
-
|
|
53387
54028
|
// ../../packages/server/tw/edge.ts
|
|
53388
54029
|
function createEdgeHandler(config) {
|
|
53389
54030
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -54102,8 +54743,11 @@ var init_tw6 = __esm({
|
|
|
54102
54743
|
init_tw2();
|
|
54103
54744
|
init_websocket_manager();
|
|
54104
54745
|
init_routing2();
|
|
54746
|
+
init_twm_rules();
|
|
54105
54747
|
init_router();
|
|
54106
54748
|
init_middleware2();
|
|
54749
|
+
init_compression();
|
|
54750
|
+
init_signal_stream();
|
|
54107
54751
|
init_static();
|
|
54108
54752
|
init_tw5();
|
|
54109
54753
|
init_ssr();
|
|
@@ -54152,6 +54796,7 @@ var init_tw6 = __esm({
|
|
|
54152
54796
|
}
|
|
54153
54797
|
ssr = null;
|
|
54154
54798
|
server = null;
|
|
54799
|
+
compressionMode = "off";
|
|
54155
54800
|
isRunning = false;
|
|
54156
54801
|
pluginManager = null;
|
|
54157
54802
|
imageHandler = null;
|
|
@@ -54168,6 +54813,40 @@ var init_tw6 = __esm({
|
|
|
54168
54813
|
const entries = Object.entries(cfgIn.redirects);
|
|
54169
54814
|
cfgIn.redirects = entries.map(([from, to]) => typeof to === "string" ? { from, to } : { from, ...to });
|
|
54170
54815
|
}
|
|
54816
|
+
if (Array.isArray(cfgIn.redirects)) {
|
|
54817
|
+
const REDIRECT_KNOWN = /* @__PURE__ */ new Set(["from", "to", "status", "source", "destination", "permanent"]);
|
|
54818
|
+
let redirectDropped = false;
|
|
54819
|
+
const normalizedRedirects = cfgIn.redirects.map((r) => {
|
|
54820
|
+
if (!r || typeof r !== "object") return r;
|
|
54821
|
+
let touched = false;
|
|
54822
|
+
const mapped = { ...r };
|
|
54823
|
+
if (mapped.from === void 0 && mapped.source !== void 0) {
|
|
54824
|
+
mapped.from = mapped.source;
|
|
54825
|
+
touched = true;
|
|
54826
|
+
}
|
|
54827
|
+
if (mapped.to === void 0 && mapped.destination !== void 0) {
|
|
54828
|
+
mapped.to = mapped.destination;
|
|
54829
|
+
touched = true;
|
|
54830
|
+
}
|
|
54831
|
+
if (mapped.status === void 0 && mapped.permanent !== void 0) {
|
|
54832
|
+
mapped.status = mapped.permanent ? 301 : 302;
|
|
54833
|
+
touched = true;
|
|
54834
|
+
}
|
|
54835
|
+
const unknown = Object.keys(mapped).filter((k) => !REDIRECT_KNOWN.has(k));
|
|
54836
|
+
if (unknown.length > 0) {
|
|
54837
|
+
console.warn("[tw] redirects: unrecognized field" + (unknown.length > 1 ? "s" : "") + " " + unknown.join(", ") + " in entry { from: " + JSON.stringify(mapped.from) + " } -- accepted fields: from, to, status");
|
|
54838
|
+
}
|
|
54839
|
+
if (typeof mapped.from !== "string" || typeof mapped.to !== "string") {
|
|
54840
|
+
console.warn("[tw] redirects: entry missing from/to after normalization -- skipped (" + JSON.stringify(r).slice(0, 80) + ")");
|
|
54841
|
+
redirectDropped = true;
|
|
54842
|
+
return null;
|
|
54843
|
+
}
|
|
54844
|
+
return touched ? mapped : r;
|
|
54845
|
+
}).filter((x) => x !== null || (redirectDropped = true));
|
|
54846
|
+
if (redirectDropped || normalizedRedirects.some((x, i) => x !== cfgIn.redirects[i])) {
|
|
54847
|
+
cfgIn.redirects = normalizedRedirects.filter(Boolean);
|
|
54848
|
+
}
|
|
54849
|
+
}
|
|
54171
54850
|
if (cfgIn.headers && !Array.isArray(cfgIn.headers)) {
|
|
54172
54851
|
const entries = Object.entries(cfgIn.headers);
|
|
54173
54852
|
cfgIn.headers = entries.map(([source, headers]) => ({ source, headers }));
|
|
@@ -54185,6 +54864,12 @@ var init_tw6 = __esm({
|
|
|
54185
54864
|
}
|
|
54186
54865
|
}
|
|
54187
54866
|
this.pluginManager = this.options.plugins ?? null;
|
|
54867
|
+
{
|
|
54868
|
+
const raw = this.options.compression ?? true;
|
|
54869
|
+
if (raw === false || raw === "none" || raw === "off" || raw === "false") this.compressionMode = "off";
|
|
54870
|
+
else if (raw === "gzip") this.compressionMode = "gzip";
|
|
54871
|
+
else this.compressionMode = "brotli";
|
|
54872
|
+
}
|
|
54188
54873
|
try {
|
|
54189
54874
|
this.imageHandler = createImageHandler({
|
|
54190
54875
|
rootDir: this.options.rootDir,
|
|
@@ -54294,31 +54979,72 @@ var init_tw6 = __esm({
|
|
|
54294
54979
|
if (existsSync13(middlewarePath)) {
|
|
54295
54980
|
this.middlewarePath = middlewarePath;
|
|
54296
54981
|
console.log(" Middleware: middleware.twm loaded");
|
|
54982
|
+
try {
|
|
54983
|
+
const src = readFileSync9(middlewarePath, "utf-8");
|
|
54984
|
+
const rules = parseRules(src, "middleware.twm");
|
|
54985
|
+
for (const r of rules) {
|
|
54986
|
+
const conds = [];
|
|
54987
|
+
if (r.userAgent) conds.push("user_agent");
|
|
54988
|
+
if (r.path) conds.push("path");
|
|
54989
|
+
if (r.auth) conds.push("auth");
|
|
54990
|
+
if (r.rateLimit) conds.push("rate_limit");
|
|
54991
|
+
if (r.origin) conds.push("origin");
|
|
54992
|
+
if (r.methods && r.methods.length > 0) conds.push("methods");
|
|
54993
|
+
const label = conds.length > 0 ? conds.join(" + ") : "no conditions -> every matching request gets this response";
|
|
54994
|
+
console.log(` \u2022 "${r.name}" [${label}]`);
|
|
54995
|
+
}
|
|
54996
|
+
} catch {
|
|
54997
|
+
}
|
|
54297
54998
|
}
|
|
54298
54999
|
return this;
|
|
54299
55000
|
}
|
|
54300
55001
|
async start() {
|
|
54301
55002
|
if (this.isRunning) return;
|
|
54302
|
-
const
|
|
54303
|
-
|
|
54304
|
-
|
|
54305
|
-
|
|
54306
|
-
|
|
54307
|
-
|
|
54308
|
-
|
|
54309
|
-
|
|
54310
|
-
|
|
54311
|
-
|
|
54312
|
-
|
|
55003
|
+
const host = this.options.host ?? "0.0.0.0";
|
|
55004
|
+
const wanted = this.options.port ?? 8e3;
|
|
55005
|
+
let port = wanted;
|
|
55006
|
+
let bound = null;
|
|
55007
|
+
let lastErr = null;
|
|
55008
|
+
for (let p = wanted; p < wanted + 10; p++) {
|
|
55009
|
+
try {
|
|
55010
|
+
bound = (typeof Bun !== "undefined" ? Bun : null)?.serve({
|
|
55011
|
+
port: p,
|
|
55012
|
+
hostname: host,
|
|
55013
|
+
fetch: async (request) => {
|
|
55014
|
+
const res = await this.handleRequest(request);
|
|
55015
|
+
let withHeaders = res;
|
|
55016
|
+
if (this.securityHeaders) {
|
|
55017
|
+
try {
|
|
55018
|
+
withHeaders = this.securityHeaders.apply(res);
|
|
55019
|
+
} catch {
|
|
55020
|
+
withHeaders = res;
|
|
55021
|
+
}
|
|
55022
|
+
}
|
|
55023
|
+
return this.compressResponse(withHeaders, request);
|
|
55024
|
+
},
|
|
55025
|
+
error: (err3) => {
|
|
55026
|
+
console.error("Server error:", err3);
|
|
55027
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
54313
55028
|
}
|
|
54314
|
-
}
|
|
54315
|
-
|
|
54316
|
-
|
|
54317
|
-
|
|
54318
|
-
|
|
54319
|
-
|
|
55029
|
+
});
|
|
55030
|
+
port = p;
|
|
55031
|
+
break;
|
|
55032
|
+
} catch (e) {
|
|
55033
|
+
lastErr = e;
|
|
55034
|
+
const msg = String(e?.message ?? e);
|
|
55035
|
+
if (!/address already in use|EADDRINUSE|Failed to start server/i.test(msg)) throw e;
|
|
55036
|
+
if (p < wanted + 9) console.log(` Port ${p} busy \u2014 trying ${p + 1}...`);
|
|
54320
55037
|
}
|
|
54321
|
-
}
|
|
55038
|
+
}
|
|
55039
|
+
if (!bound) {
|
|
55040
|
+
console.error(`
|
|
55041
|
+
ERROR: could not bind ${host} on ports ${wanted}\u2013${wanted + 9}.`);
|
|
55042
|
+
console.error(` ${lastErr?.message ?? lastErr}
|
|
55043
|
+
`);
|
|
55044
|
+
throw lastErr ?? new Error("could not start server");
|
|
55045
|
+
}
|
|
55046
|
+
this.server = bound;
|
|
55047
|
+
this.options.port = port;
|
|
54322
55048
|
this.isRunning = true;
|
|
54323
55049
|
console.log(`
|
|
54324
55050
|
TW Server running at http://${host}:${port}
|
|
@@ -54332,6 +55058,10 @@ var init_tw6 = __esm({
|
|
|
54332
55058
|
// finishes ongoing requests, then closes
|
|
54333
55059
|
async gracefulShutdown(timeout = 5e3) {
|
|
54334
55060
|
this.isRunning = false;
|
|
55061
|
+
try {
|
|
55062
|
+
getSignalHub().closeAll();
|
|
55063
|
+
} catch {
|
|
55064
|
+
}
|
|
54335
55065
|
if (this.server) {
|
|
54336
55066
|
this.server.stop?.(false);
|
|
54337
55067
|
const start = Date.now();
|
|
@@ -54357,6 +55087,35 @@ var init_tw6 = __esm({
|
|
|
54357
55087
|
* - return { headers: {...} } -> headers merged into the final response
|
|
54358
55088
|
* - return null/undefined -> continue
|
|
54359
55089
|
*/
|
|
55090
|
+
/** BUG 7 (v1.0.8): real response compression. `compression` in
|
|
55091
|
+
* tw.config.ts was silently ignored -- the middleware was a no-op and
|
|
55092
|
+
* nothing on the response path ever set Content-Encoding. */
|
|
55093
|
+
async compressResponse(res, request) {
|
|
55094
|
+
if (this.compressionMode === "off") return res;
|
|
55095
|
+
try {
|
|
55096
|
+
if (res.headers.get("content-encoding")) return res;
|
|
55097
|
+
const ct = res.headers.get("content-type") || "";
|
|
55098
|
+
if (ct.includes("text/event-stream")) return res;
|
|
55099
|
+
if (res.__twRawStream) return res;
|
|
55100
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
55101
|
+
if (!isLargeEnough(buf.length)) return new Response(buf, { status: res.status, headers: res.headers });
|
|
55102
|
+
const accept = request.headers.get("accept-encoding") || "";
|
|
55103
|
+
let enc = negotiateEncoding(accept);
|
|
55104
|
+
if (this.compressionMode === "gzip" && enc === "brotli") enc = "gzip";
|
|
55105
|
+
if (!enc) return new Response(buf, { status: res.status, headers: res.headers });
|
|
55106
|
+
const out = await compress(buf, enc);
|
|
55107
|
+
if (out.length >= buf.length) return new Response(buf, { status: res.status, headers: res.headers });
|
|
55108
|
+
const h = new Headers(res.headers);
|
|
55109
|
+
h.set("Content-Encoding", enc === "brotli" ? "br" : enc);
|
|
55110
|
+
h.set("Vary", "Accept-Encoding");
|
|
55111
|
+
h.delete("Content-Length");
|
|
55112
|
+
return new Response(out, { status: res.status, headers: h });
|
|
55113
|
+
} catch {
|
|
55114
|
+
return res;
|
|
55115
|
+
}
|
|
55116
|
+
}
|
|
55117
|
+
/** v1.0.8 round 5 (BUG 39): public now -- serverless adapters (Vercel
|
|
55118
|
+
* functions, edge runtimes) drive the server through this entry. */
|
|
54360
55119
|
async handleRequest(request) {
|
|
54361
55120
|
try {
|
|
54362
55121
|
const u = new URL(request.url);
|
|
@@ -54529,69 +55288,12 @@ var init_tw6 = __esm({
|
|
|
54529
55288
|
* `data: {"v":1,"seq":N,"updates":[...]}` frames.
|
|
54530
55289
|
*/
|
|
54531
55290
|
handleSignalStream(request, url) {
|
|
54532
|
-
const
|
|
54533
|
-
|
|
54534
|
-
|
|
54535
|
-
|
|
54536
|
-
|
|
54537
|
-
|
|
54538
|
-
try {
|
|
54539
|
-
const header = request.headers.get("cookie") ?? "";
|
|
54540
|
-
const m = /(?:^|;\s*)tw_session=([^;]+)/.exec(header);
|
|
54541
|
-
if (m) session = decodeURIComponent(m[1].trim().replace(/^"|"$/g, ""));
|
|
54542
|
-
} catch {
|
|
54543
|
-
}
|
|
54544
|
-
const enc = new TextEncoder();
|
|
54545
|
-
const sse = (payload) => enc.encode("data: " + payload + "\n\n");
|
|
54546
|
-
const stream = new ReadableStream({
|
|
54547
|
-
start(controller) {
|
|
54548
|
-
const client = hub2.register(declared, session);
|
|
54549
|
-
client.send = (payload) => {
|
|
54550
|
-
try {
|
|
54551
|
-
controller.enqueue(sse(payload));
|
|
54552
|
-
} catch {
|
|
54553
|
-
client.close();
|
|
54554
|
-
}
|
|
54555
|
-
};
|
|
54556
|
-
client.close = () => {
|
|
54557
|
-
try {
|
|
54558
|
-
controller.close();
|
|
54559
|
-
} catch {
|
|
54560
|
-
}
|
|
54561
|
-
hub2["clients"];
|
|
54562
|
-
};
|
|
54563
|
-
for (const payload of hub2.connectPayloads(since, declared, session)) {
|
|
54564
|
-
try {
|
|
54565
|
-
controller.enqueue(sse(payload));
|
|
54566
|
-
} catch {
|
|
54567
|
-
break;
|
|
54568
|
-
}
|
|
54569
|
-
}
|
|
54570
|
-
const keepAlive = setInterval(() => {
|
|
54571
|
-
try {
|
|
54572
|
-
controller.enqueue(enc.encode(": keep-alive\n\n"));
|
|
54573
|
-
} catch {
|
|
54574
|
-
clearInterval(keepAlive);
|
|
54575
|
-
}
|
|
54576
|
-
}, 25e3);
|
|
54577
|
-
client.close = () => {
|
|
54578
|
-
clearInterval(keepAlive);
|
|
54579
|
-
try {
|
|
54580
|
-
controller.close();
|
|
54581
|
-
} catch {
|
|
54582
|
-
}
|
|
54583
|
-
};
|
|
54584
|
-
}
|
|
54585
|
-
});
|
|
54586
|
-
return new Response(stream, {
|
|
54587
|
-
status: 200,
|
|
54588
|
-
headers: {
|
|
54589
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
54590
|
-
"Cache-Control": "no-store",
|
|
54591
|
-
"X-Accel-Buffering": "no",
|
|
54592
|
-
Connection: "keep-alive"
|
|
54593
|
-
}
|
|
54594
|
-
});
|
|
55291
|
+
const cfg = this.options.config?.signalStream;
|
|
55292
|
+
return createSignalStream(request, url, cfg ? { maxClients: cfg.maxClients, maxHistory: cfg.maxHistory } : void 0);
|
|
55293
|
+
}
|
|
55294
|
+
async handleClientSignalWrite(request) {
|
|
55295
|
+
const cfg = this.options.config?.signalStream;
|
|
55296
|
+
return writeSignalFromClient(request, { clientWrites: cfg?.clientWrites });
|
|
54595
55297
|
}
|
|
54596
55298
|
/**
|
|
54597
55299
|
* PPR hole fill: render the page fresh and return only the requested
|
|
@@ -54665,15 +55367,33 @@ var init_tw6 = __esm({
|
|
|
54665
55367
|
controller.close();
|
|
54666
55368
|
}
|
|
54667
55369
|
});
|
|
54668
|
-
|
|
55370
|
+
const streamRes = new Response(stream, {
|
|
54669
55371
|
status: 200,
|
|
54670
55372
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
54671
55373
|
});
|
|
55374
|
+
streamRes.__twRawStream = true;
|
|
55375
|
+
return streamRes;
|
|
54672
55376
|
}
|
|
54673
55377
|
async handleRequestInner(request) {
|
|
54674
55378
|
const url = new URL(request.url);
|
|
54675
55379
|
const method = request.method;
|
|
54676
|
-
|
|
55380
|
+
let rawPath = url.pathname;
|
|
55381
|
+
try {
|
|
55382
|
+
const decoded = decodeURIComponent(rawPath);
|
|
55383
|
+
if (decoded !== rawPath) rawPath = decoded;
|
|
55384
|
+
} catch {
|
|
55385
|
+
}
|
|
55386
|
+
let canonical = normalizePathname(rawPath);
|
|
55387
|
+
if (canonical.length > 1 && canonical.endsWith("/")) {
|
|
55388
|
+
canonical = canonical.slice(0, -1);
|
|
55389
|
+
}
|
|
55390
|
+
if (canonical !== rawPath) {
|
|
55391
|
+
return new Response(null, {
|
|
55392
|
+
status: 308,
|
|
55393
|
+
headers: { Location: encodeURI(canonical) + url.search }
|
|
55394
|
+
});
|
|
55395
|
+
}
|
|
55396
|
+
const pathname = canonical;
|
|
54677
55397
|
if (pathname === "/_tw/img" || pathname.startsWith("/_tw/img/")) {
|
|
54678
55398
|
const imgRes = await this.imageHandler.handle(request);
|
|
54679
55399
|
if (imgRes) return imgRes;
|
|
@@ -54681,6 +55401,9 @@ var init_tw6 = __esm({
|
|
|
54681
55401
|
if (pathname === "/_tw/stream") {
|
|
54682
55402
|
return this.handleSignalStream(request, url);
|
|
54683
55403
|
}
|
|
55404
|
+
if (pathname === "/_tw/signal" && method === "POST") {
|
|
55405
|
+
return this.handleClientSignalWrite(request);
|
|
55406
|
+
}
|
|
54684
55407
|
if (pathname === "/_tw/ppr") {
|
|
54685
55408
|
return this.handlePPRHoleFill(url);
|
|
54686
55409
|
}
|
|
@@ -54845,13 +55568,39 @@ __export(twm_loader_exports, {
|
|
|
54845
55568
|
loadTWMModule: () => loadTWMModule,
|
|
54846
55569
|
revalidateRoute: () => revalidateRoute,
|
|
54847
55570
|
shouldMatchMiddleware: () => shouldMatchMiddleware,
|
|
54848
|
-
stripCommentsSafe: () => stripCommentsSafe
|
|
55571
|
+
stripCommentsSafe: () => stripCommentsSafe,
|
|
55572
|
+
validateTWMSyntax: () => validateTWMSyntax
|
|
54849
55573
|
});
|
|
54850
55574
|
import { readFileSync as readFileSync10, existsSync as existsSync14 } from "node:fs";
|
|
54851
55575
|
import { join as join18 } from "node:path";
|
|
54852
55576
|
function clearTWMCache() {
|
|
54853
55577
|
moduleCache.clear();
|
|
54854
55578
|
}
|
|
55579
|
+
function validateTWMSyntax(filePath) {
|
|
55580
|
+
let source;
|
|
55581
|
+
try {
|
|
55582
|
+
source = readFileSync10(filePath, "utf-8");
|
|
55583
|
+
} catch (err3) {
|
|
55584
|
+
return "cannot read: " + (err3?.message ?? err3);
|
|
55585
|
+
}
|
|
55586
|
+
if (isRuleDsl(source)) {
|
|
55587
|
+
try {
|
|
55588
|
+
parseRules(source, filePath);
|
|
55589
|
+
return null;
|
|
55590
|
+
} catch (err3) {
|
|
55591
|
+
return "rule DSL: " + (err3?.message ?? String(err3));
|
|
55592
|
+
}
|
|
55593
|
+
}
|
|
55594
|
+
try {
|
|
55595
|
+
const imports = extractImports(source);
|
|
55596
|
+
const jsCode = compileTwm(source);
|
|
55597
|
+
const codeWithoutImports = stripImports(jsCode);
|
|
55598
|
+
new Function("__tw_unused", codeWithoutImports);
|
|
55599
|
+
return null;
|
|
55600
|
+
} catch (err3) {
|
|
55601
|
+
return (err3?.message ?? String(err3)).replace(/\n/g, " ").slice(0, 200);
|
|
55602
|
+
}
|
|
55603
|
+
}
|
|
54855
55604
|
function stripCommentsSafe(src) {
|
|
54856
55605
|
let out = "";
|
|
54857
55606
|
let i = 0;
|
|
@@ -55089,7 +55838,7 @@ async function loadTWMModule(filePath, rootDir) {
|
|
|
55089
55838
|
console.error("[tw] " + e.message + " (" + filePath + ":" + cm[1] + ")");
|
|
55090
55839
|
}
|
|
55091
55840
|
}
|
|
55092
|
-
const moduleCode = codeWithoutImports + "\nreturn { get: typeof get !== 'undefined' ? get : null, post: typeof post !== 'undefined' ? post : null, put: typeof put !== 'undefined' ? put : null, patch: typeof patch !== 'undefined' ? patch : null, deleteFn: typeof deleteFn !== 'undefined' ? deleteFn : null, GET: typeof GET !== 'undefined' ? GET : null, POST: typeof POST !== 'undefined' ? POST : null, PUT: typeof PUT !== 'undefined' ? PUT : null, PATCH: typeof PATCH !== 'undefined' ? PATCH : null, DELETE: typeof DELETE !== 'undefined' ? DELETE : null, middleware: typeof middleware !== 'undefined' ? middleware : null, generate: typeof generate !== 'undefined' ? generate : null, config: typeof config !== 'undefined' ? config : null, action: typeof action !== 'undefined' ? action : null, revalidate: typeof revalidate !== 'undefined' ? revalidate : null, " + [...actionNames].map((n) => `${n}: typeof ${n} !== 'undefined' ? ${n} : null`).join(", ") + " };";
|
|
55841
|
+
const moduleCode = codeWithoutImports + "\nreturn { get: typeof get !== 'undefined' ? get : null, post: typeof post !== 'undefined' ? post : null, put: typeof put !== 'undefined' ? put : null, patch: typeof patch !== 'undefined' ? patch : null, deleteFn: typeof deleteFn !== 'undefined' ? deleteFn : null, GET: typeof GET !== 'undefined' ? GET : null, POST: typeof POST !== 'undefined' ? POST : null, PUT: typeof PUT !== 'undefined' ? PUT : null, PATCH: typeof PATCH !== 'undefined' ? PATCH : null, DELETE: typeof DELETE !== 'undefined' ? DELETE : null, options: typeof options !== 'undefined' ? options : null, OPTIONS: typeof OPTIONS !== 'undefined' ? OPTIONS : null, middleware: typeof middleware !== 'undefined' ? middleware : null, generate: typeof generate !== 'undefined' ? generate : null, config: typeof config !== 'undefined' ? config : null, action: typeof action !== 'undefined' ? action : null, revalidate: typeof revalidate !== 'undefined' ? revalidate : null, " + [...actionNames].map((n) => `${n}: typeof ${n} !== 'undefined' ? ${n} : null`).join(", ") + " };";
|
|
55093
55842
|
const injectedKeys = Object.keys(injected);
|
|
55094
55843
|
let module = {};
|
|
55095
55844
|
try {
|
|
@@ -55104,6 +55853,8 @@ async function loadTWMModule(filePath, rootDir) {
|
|
|
55104
55853
|
put: H.put || H.PUT || void 0,
|
|
55105
55854
|
delete: H.deleteFn || H.DELETE || void 0,
|
|
55106
55855
|
patch: H.patch || H.PATCH || void 0,
|
|
55856
|
+
// v1.0.8 round 4 (BUG 33): route-level OPTIONS handlers
|
|
55857
|
+
options: H.options || H.OPTIONS || void 0,
|
|
55107
55858
|
middleware: H.middleware || void 0,
|
|
55108
55859
|
generate: H.generate || void 0,
|
|
55109
55860
|
config: H.config || void 0,
|
|
@@ -55239,9 +55990,22 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55239
55990
|
if (rawBody2.length > MAX_BODY_BYTES) {
|
|
55240
55991
|
return { status: 413, json: { ok: false, error: "Payload too large" } };
|
|
55241
55992
|
}
|
|
55242
|
-
|
|
55993
|
+
if (!rawBody2) {
|
|
55994
|
+
parsedBody = {};
|
|
55995
|
+
} else if (__ct.includes("application/x-www-form-urlencoded")) {
|
|
55996
|
+
parsedBody = Object.fromEntries(new URLSearchParams(rawBody2).entries());
|
|
55997
|
+
} else if (__ct.startsWith("text/")) {
|
|
55998
|
+
parsedBody = rawBody2;
|
|
55999
|
+
} else if (__ct.includes("json") || __ct === "") {
|
|
56000
|
+
try {
|
|
56001
|
+
parsedBody = JSON.parse(rawBody2);
|
|
56002
|
+
} catch (e) {
|
|
56003
|
+
return { status: 400, json: { ok: false, error: "Invalid JSON body: " + (e?.message ?? "parse error") } };
|
|
56004
|
+
}
|
|
56005
|
+
} else {
|
|
56006
|
+
return { status: 415, json: { ok: false, error: "Unsupported Content-Type: " + (__ct || "(none)") } };
|
|
56007
|
+
}
|
|
55243
56008
|
} catch {
|
|
55244
|
-
parsedBody = {};
|
|
55245
56009
|
}
|
|
55246
56010
|
} else if (request?.body && typeof request.body === "object") {
|
|
55247
56011
|
parsedBody = request.body;
|
|
@@ -55249,7 +56013,18 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55249
56013
|
let query2 = {};
|
|
55250
56014
|
try {
|
|
55251
56015
|
const u = new URL(request.url);
|
|
55252
|
-
|
|
56016
|
+
for (const [rawKey, value] of u.searchParams.entries()) {
|
|
56017
|
+
const isPhpArray = rawKey.endsWith("[]");
|
|
56018
|
+
const key = isPhpArray ? rawKey.slice(0, -2) : rawKey;
|
|
56019
|
+
const existing = query2[key];
|
|
56020
|
+
if (existing === void 0) {
|
|
56021
|
+
query2[key] = isPhpArray ? [value] : value;
|
|
56022
|
+
} else if (Array.isArray(existing)) {
|
|
56023
|
+
existing.push(value);
|
|
56024
|
+
} else {
|
|
56025
|
+
query2[key] = [existing, value];
|
|
56026
|
+
}
|
|
56027
|
+
}
|
|
55253
56028
|
} catch {
|
|
55254
56029
|
}
|
|
55255
56030
|
const headersObj = typeof request.headers?.entries === "function" ? Object.fromEntries(request.headers.entries()) : request.headers ?? {};
|
|
@@ -55378,9 +56153,17 @@ async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
|
55378
56153
|
console.error("[tw] route handler error (" + filePath + "):", err3?.message ?? err3);
|
|
55379
56154
|
return { status: 500, json: { ok: false, error: "Internal Server Error" } };
|
|
55380
56155
|
}
|
|
55381
|
-
if (
|
|
55382
|
-
|
|
55383
|
-
|
|
56156
|
+
if (typeof result === "string") {
|
|
56157
|
+
result = { status: 200, text: result };
|
|
56158
|
+
} else if (!result || typeof result !== "object") {
|
|
56159
|
+
console.error("[tw] route handler returned " + JSON.stringify(result) + " (" + filePath + ")");
|
|
56160
|
+
return {
|
|
56161
|
+
status: 500,
|
|
56162
|
+
json: {
|
|
56163
|
+
ok: false,
|
|
56164
|
+
error: "Unsupported handler return shape: " + (result === null ? "null" : typeof result) + ". Return { status, json|text|html } (or a plain string for text/plain)."
|
|
56165
|
+
}
|
|
56166
|
+
};
|
|
55384
56167
|
}
|
|
55385
56168
|
const finalResult = {
|
|
55386
56169
|
status: typeof result.status === "number" ? result.status : 200,
|
|
@@ -55735,11 +56518,18 @@ async function buildCommand() {
|
|
|
55735
56518
|
} catch {
|
|
55736
56519
|
}
|
|
55737
56520
|
let pageCount = 0;
|
|
56521
|
+
let requestRenderedCount = 0;
|
|
55738
56522
|
let apiCount = 0;
|
|
55739
56523
|
let errorCount = 0;
|
|
55740
56524
|
const isrRoutes = /* @__PURE__ */ new Map();
|
|
55741
56525
|
const pageFiles = [];
|
|
55742
56526
|
collectFiles(homeDir, "page.tw", pageFiles);
|
|
56527
|
+
for (const pf of pageFiles) {
|
|
56528
|
+
if (existsSync16(join20(dirname6(pf), "index.tw"))) {
|
|
56529
|
+
console.log(" \x1B[33m!\x1B[0m " + dirname6(pf).replace(homeDir, "").replace(/^\//, "") + "/ has BOTH page.tw and index.tw");
|
|
56530
|
+
console.log(" index.tw wins this route -- delete one of them");
|
|
56531
|
+
}
|
|
56532
|
+
}
|
|
55743
56533
|
begin("compile", pageFiles.length);
|
|
55744
56534
|
const htmlOutputs = [];
|
|
55745
56535
|
_compilerMod.clearCssCapture?.();
|
|
@@ -55757,11 +56547,20 @@ async function buildCommand() {
|
|
|
55757
56547
|
const pmod = await loadTWMModule2(paramsTwm, rootDir);
|
|
55758
56548
|
if (typeof pmod.generate === "function") {
|
|
55759
56549
|
const arr = pmod.generate();
|
|
55760
|
-
if (Array.isArray(arr) && arr.length > 0)
|
|
55761
|
-
const
|
|
55762
|
-
|
|
55763
|
-
|
|
55764
|
-
|
|
56550
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
56551
|
+
const dynNames = pageFile.split("/").map((seg) => /^\[([^\]]+)\]$/.exec(seg)?.[1]).filter(Boolean);
|
|
56552
|
+
paramSets = arr.map((x) => {
|
|
56553
|
+
if (x !== null && typeof x !== "object") {
|
|
56554
|
+
const o2 = {};
|
|
56555
|
+
const key = dynNames[0] ?? "slug";
|
|
56556
|
+
o2[key] = String(x);
|
|
56557
|
+
return o2;
|
|
56558
|
+
}
|
|
56559
|
+
const o = {};
|
|
56560
|
+
for (const [k, v] of Object.entries(x)) o[k] = String(v);
|
|
56561
|
+
return o;
|
|
56562
|
+
});
|
|
56563
|
+
}
|
|
55765
56564
|
}
|
|
55766
56565
|
} catch (e) {
|
|
55767
56566
|
console.log(" Warning: params.twm failed: " + e.message);
|
|
@@ -55812,6 +56611,7 @@ async function buildCommand() {
|
|
|
55812
56611
|
}
|
|
55813
56612
|
const hasExplicitParams = paramSets.length > 1 || Object.keys(paramSets[0] ?? {}).length > 0;
|
|
55814
56613
|
if ((renderMode === "ssr" || renderMode === "stream") && !hasExplicitParams) {
|
|
56614
|
+
requestRenderedCount++;
|
|
55815
56615
|
console.log(" \x1B[36m~\x1B[0m " + routePath + " [" + renderMode + ": rendered at request time]");
|
|
55816
56616
|
try {
|
|
55817
56617
|
const src = readFileSync12(pageFile, "utf-8");
|
|
@@ -55822,6 +56622,29 @@ async function buildCommand() {
|
|
|
55822
56622
|
for (const d of errs.slice(0, 5)) console.log(" " + d.line + ":" + d.col + " " + d.message);
|
|
55823
56623
|
errorCount++;
|
|
55824
56624
|
}
|
|
56625
|
+
const warns = (r.diagnostics || []).filter((d) => d.severity !== "error");
|
|
56626
|
+
if (warns.length > 0) {
|
|
56627
|
+
console.log(" \x1B[33m" + warns.length + " warning" + (warns.length === 1 ? "" : "s") + "\x1B[0m");
|
|
56628
|
+
for (const d of warns.slice(0, 3)) {
|
|
56629
|
+
console.log(" " + d.line + ":" + d.col + " " + d.code + " " + d.message);
|
|
56630
|
+
}
|
|
56631
|
+
if (warns.length > 3) console.log(" ... " + (warns.length - 3) + " more (tw check)");
|
|
56632
|
+
}
|
|
56633
|
+
{
|
|
56634
|
+
const used = /* @__PURE__ */ new Set();
|
|
56635
|
+
collectComponentUsages(r.ast, used);
|
|
56636
|
+
const BUILTIN_ALIASES = /* @__PURE__ */ new Set(["Link", "RouterLink", "Image", "optImage", "Suspense"]);
|
|
56637
|
+
const unknownSsr = [...used].filter((t2) => !BUILTIN_ALIASES.has(t2) && !registeredComponentNames.has(t2));
|
|
56638
|
+
if (unknownSsr.length > 0) {
|
|
56639
|
+
const importSpecs = /* @__PURE__ */ new Map();
|
|
56640
|
+
for (const im of src.matchAll(/import\s+([A-Za-z_]\w*)\s+from\s+["']([^"']+)["']/g)) {
|
|
56641
|
+
importSpecs.set(im[1], im[2]);
|
|
56642
|
+
}
|
|
56643
|
+
const hints = unknownSsr.map((t2) => importSpecs.has(t2) ? `<${t2}> (imported from ${importSpecs.get(t2)} -- file not found)` : `<${t2}> (no components/${t2}.tw found)`);
|
|
56644
|
+
console.log(" \x1B[31m\u2717 " + routePath + ": unknown component" + (unknownSsr.length > 1 ? "s" : "") + " " + hints.join(", ") + "\x1B[0m");
|
|
56645
|
+
errorCount++;
|
|
56646
|
+
}
|
|
56647
|
+
}
|
|
55825
56648
|
} catch (e) {
|
|
55826
56649
|
console.log(" \x1B[33m! " + routePath + ": " + (e && e.message ? e.message : e) + "\x1B[0m");
|
|
55827
56650
|
errorCount++;
|
|
@@ -55840,7 +56663,12 @@ async function buildCommand() {
|
|
|
55840
56663
|
const BUILTIN_ALIASES = /* @__PURE__ */ new Set(["Link", "RouterLink", "Image", "optImage", "Suspense"]);
|
|
55841
56664
|
const unknown = [...used].filter((t2) => !BUILTIN_ALIASES.has(t2) && !registeredComponentNames.has(t2));
|
|
55842
56665
|
if (unknown.length > 0) {
|
|
55843
|
-
|
|
56666
|
+
const importSpecs = /* @__PURE__ */ new Map();
|
|
56667
|
+
for (const im of source.matchAll(/import\s+([A-Za-z_]\w*)\s+from\s+["']([^"']+)["']/g)) {
|
|
56668
|
+
importSpecs.set(im[1], im[2]);
|
|
56669
|
+
}
|
|
56670
|
+
const hints = unknown.map((t2) => importSpecs.has(t2) ? `<${t2}> (imported from ${importSpecs.get(t2)} -- file not found)` : `<${t2}> (no components/${t2}.tw found)`);
|
|
56671
|
+
console.error(" \x1B[31m\u2717 " + pageFile.replace(rootDir + "/", "") + ": unknown component" + (unknown.length > 1 ? "s" : "") + " " + hints.join(", ") + " -- create the file or fix the import\x1B[0m");
|
|
55844
56672
|
process.exit(1);
|
|
55845
56673
|
}
|
|
55846
56674
|
}
|
|
@@ -55932,9 +56760,12 @@ async function buildCommand() {
|
|
|
55932
56760
|
if (pu) clientTag += `<script defer src="/${pu}"></script>
|
|
55933
56761
|
`;
|
|
55934
56762
|
}
|
|
55935
|
-
const
|
|
55936
|
-
|
|
55937
|
-
|
|
56763
|
+
const needsClient = interactive || pageMods.deps.length > 0 || pageMods.own.length > 0 || renderMode === "csr" || renderMode === "ppr" || renderMode === "stream" || renderMode === "signalStream";
|
|
56764
|
+
if (needsClient) {
|
|
56765
|
+
const runtimeTag = `<script defer src="/__tw_runtime.js"></script>`;
|
|
56766
|
+
html = html.replace("</body>", " " + clientTag + runtimeTag + "\n</body>");
|
|
56767
|
+
needsRuntime = true;
|
|
56768
|
+
}
|
|
55938
56769
|
}
|
|
55939
56770
|
if (renderMode === "csr") {
|
|
55940
56771
|
html = csrifyHtml(html);
|
|
@@ -55989,6 +56820,14 @@ async function buildCommand() {
|
|
|
55989
56820
|
} else {
|
|
55990
56821
|
console.log(" \x1B[32mOK\x1B[0m " + displayPath + " -> " + (outRoute === "" ? ".tw/index.html" : ".tw/" + outRoute + "/index.html"));
|
|
55991
56822
|
}
|
|
56823
|
+
const warnings = result.diagnostics.filter((d) => d.severity !== "error");
|
|
56824
|
+
if (warnings.length > 0) {
|
|
56825
|
+
console.log(" \x1B[33m" + warnings.length + " warning" + (warnings.length === 1 ? "" : "s") + "\x1B[0m");
|
|
56826
|
+
for (const d of warnings.slice(0, 3)) {
|
|
56827
|
+
console.log(" " + d.line + ":" + d.col + " " + d.code + " " + d.message);
|
|
56828
|
+
}
|
|
56829
|
+
if (warnings.length > 3) console.log(" ... " + (warnings.length - 3) + " more (tw check)");
|
|
56830
|
+
}
|
|
55992
56831
|
pageCount++;
|
|
55993
56832
|
} catch (err3) {
|
|
55994
56833
|
console.log(" \x1B[31mX " + routePath + ": " + err3.message + "\x1B[0m");
|
|
@@ -56007,8 +56846,16 @@ async function buildCommand() {
|
|
|
56007
56846
|
const outputDir = routeName === "" ? outDir : join20(outDir, routeName);
|
|
56008
56847
|
mkdirSync6(outputDir, { recursive: true });
|
|
56009
56848
|
copyFileSync2(twmFile, join20(outputDir, "route.twm"));
|
|
56010
|
-
|
|
56011
|
-
|
|
56849
|
+
const { validateTWMSyntax: validateTWMSyntax2 } = await Promise.resolve().then(() => (init_twm_loader(), twm_loader_exports));
|
|
56850
|
+
const syntaxError = validateTWMSyntax2(twmFile);
|
|
56851
|
+
if (syntaxError) {
|
|
56852
|
+
console.log(" \x1B[31mX\x1B[0m /" + routeName + " [api] -- .twm syntax error");
|
|
56853
|
+
console.log(" " + twmFile.replace(rootDir + "/", "") + ": " + syntaxError);
|
|
56854
|
+
errorCount++;
|
|
56855
|
+
} else {
|
|
56856
|
+
console.log(" \x1B[32mOK\x1B[0m /" + routeName + " [api]");
|
|
56857
|
+
apiCount++;
|
|
56858
|
+
}
|
|
56012
56859
|
}
|
|
56013
56860
|
if (existsSync16(libDir)) {
|
|
56014
56861
|
copyDir2(libDir, join20(outDir, "lib"));
|
|
@@ -56087,7 +56934,7 @@ async function buildCommand() {
|
|
|
56087
56934
|
}
|
|
56088
56935
|
}
|
|
56089
56936
|
console.log("\n \x1B[32mBuild complete!\x1B[0m");
|
|
56090
|
-
console.log(" Pages: " + pageCount + " compiled");
|
|
56937
|
+
console.log(" Pages: " + pageCount + " compiled" + (requestRenderedCount > 0 ? " (+" + requestRenderedCount + " rendered at request time)" : ""));
|
|
56091
56938
|
if (pageCount === 0) console.warn(" [!] No static pages were compiled -- every page is SSR/API. Static hosts (Vercel/Cloudflare Pages output) will serve an empty site. Use `render static` or check your render modes.");
|
|
56092
56939
|
console.log(" APIs: " + apiCount + " routes");
|
|
56093
56940
|
if (isrRoutes.size > 0) {
|
|
@@ -60191,15 +61038,37 @@ ${name}/
|
|
|
60191
61038
|
async function createVercelJson(dir) {
|
|
60192
61039
|
const config = {
|
|
60193
61040
|
buildCommand: "npx tw build",
|
|
60194
|
-
|
|
60195
|
-
|
|
60196
|
-
|
|
61041
|
+
installCommand: "npm install",
|
|
61042
|
+
functions: {
|
|
61043
|
+
"api/tw-server.ts": { memory: 512 }
|
|
61044
|
+
},
|
|
61045
|
+
rewrites: [
|
|
61046
|
+
{ source: "/(.*)", destination: "/api/tw-server" }
|
|
61047
|
+
]
|
|
60197
61048
|
};
|
|
60198
61049
|
await writeFile(join(dir, "vercel.json"), JSON.stringify(config, null, 2));
|
|
60199
61050
|
console.log(" \u2713 vercel.json");
|
|
61051
|
+
const fnDir = join(dir, "api");
|
|
61052
|
+
await mkdir(fnDir, { recursive: true });
|
|
61053
|
+
await writeFile(join(fnDir, "tw-server.ts"), [
|
|
61054
|
+
"// TW Framework -- Vercel serverless entry (v1.0.8 round 5, BUG 39).",
|
|
61055
|
+
"// Runs the full TW server (SSR pages + .twm API routes) as a Vercel",
|
|
61056
|
+
"// function. vercel.json rewrites every route here.",
|
|
61057
|
+
'import { TWServer } from "tw-framework/server";',
|
|
61058
|
+
"",
|
|
61059
|
+
"const server = new TWServer({",
|
|
61060
|
+
" rootDir: process.cwd(),",
|
|
61061
|
+
"});",
|
|
61062
|
+
"",
|
|
61063
|
+
"export default async function handler(request: Request): Promise<Response> {",
|
|
61064
|
+
" return server.handleRequest(request);",
|
|
61065
|
+
"}",
|
|
61066
|
+
""
|
|
61067
|
+
].join("\n"));
|
|
61068
|
+
console.log(" \u2713 api/tw-server.ts");
|
|
60200
61069
|
}
|
|
60201
61070
|
async function createDefaultProject(dir, name) {
|
|
60202
|
-
const T = (s) => s;
|
|
61071
|
+
const T = (s) => s.split("${name}").join(name);
|
|
60203
61072
|
for (const d of [
|
|
60204
61073
|
["home"],
|
|
60205
61074
|
["home", "about"],
|
|
@@ -60222,8 +61091,6 @@ html {
|
|
|
60222
61091
|
head {
|
|
60223
61092
|
meta charset "utf-8"
|
|
60224
61093
|
meta name "viewport" content "width=device-width, initial-scale=1"
|
|
60225
|
-
meta name "description" content "{page.description}"
|
|
60226
|
-
title "{page.title} -- ${"$"}{name}"
|
|
60227
61094
|
}
|
|
60228
61095
|
body {
|
|
60229
61096
|
header.sitenav {
|
|
@@ -60559,6 +61426,10 @@ a:hover { td underline }
|
|
|
60559
61426
|
// This sample blocks known bot user agents everywhere. Guards are
|
|
60560
61427
|
// fail-closed: a matching request that fails the condition gets the
|
|
60561
61428
|
// rule's response, and no page code ever runs.
|
|
61429
|
+
//
|
|
61430
|
+
// Note: a rule with ONLY match + response (no condition blocks) applies
|
|
61431
|
+
// to EVERY matching request -- e.g. a site-wide maintenance page. tw serve
|
|
61432
|
+
// lists each rule at startup and flags those unconditional ones.
|
|
60562
61433
|
|
|
60563
61434
|
rule "block bots" {
|
|
60564
61435
|
match "/**"
|
|
@@ -61522,6 +62393,7 @@ async function devCommand() {
|
|
|
61522
62393
|
}
|
|
61523
62394
|
let devRedirects = [];
|
|
61524
62395
|
let devCfgHeaders = [];
|
|
62396
|
+
let devSigCfg = void 0;
|
|
61525
62397
|
try {
|
|
61526
62398
|
const cfgFile = join19(rootDir, "tw.config.ts");
|
|
61527
62399
|
if (existsSync15(cfgFile)) {
|
|
@@ -61534,12 +62406,24 @@ async function devCommand() {
|
|
|
61534
62406
|
if (cfg?.headers) {
|
|
61535
62407
|
devCfgHeaders = Array.isArray(cfg.headers) ? cfg.headers : Object.entries(cfg.headers).map(([source, headers]) => ({ source, headers }));
|
|
61536
62408
|
}
|
|
62409
|
+
if (cfg?.signalStream) devSigCfg = cfg.signalStream;
|
|
61537
62410
|
}
|
|
61538
62411
|
} catch {
|
|
61539
62412
|
}
|
|
61540
62413
|
const devPathMatch = (pattern, pathname) => pattern === pathname || pattern.endsWith("/**") && pathname.startsWith(pattern.slice(0, -2) + "/") || pattern.endsWith("/") && pathname.startsWith(pattern) || pattern === "/**" && pathname.startsWith("/");
|
|
61541
62414
|
async function devFetchInner(req) {
|
|
61542
62415
|
const url = new URL(req.url);
|
|
62416
|
+
try {
|
|
62417
|
+
let rawPath = url.pathname;
|
|
62418
|
+
const decoded = decodeURIComponent(rawPath);
|
|
62419
|
+
if (decoded !== rawPath) rawPath = decoded;
|
|
62420
|
+
let canonical = rawPath.replace(/\/{2,}/g, "/");
|
|
62421
|
+
if (canonical.length > 1 && canonical.endsWith("/")) canonical = canonical.slice(0, -1);
|
|
62422
|
+
if (canonical !== rawPath) {
|
|
62423
|
+
return new Response(null, { status: 308, headers: { Location: encodeURI(canonical) + url.search } });
|
|
62424
|
+
}
|
|
62425
|
+
} catch {
|
|
62426
|
+
}
|
|
61543
62427
|
const path = url.pathname;
|
|
61544
62428
|
const time = (/* @__PURE__ */ new Date()).toISOString().split("T")[1].split(".")[0];
|
|
61545
62429
|
rescanComponentsIfNeeded(rootDir);
|
|
@@ -61819,56 +62703,108 @@ async function devCommand() {
|
|
|
61819
62703
|
headers: { "Content-Type": "text/html" }
|
|
61820
62704
|
});
|
|
61821
62705
|
}
|
|
61822
|
-
|
|
61823
|
-
|
|
61824
|
-
|
|
61825
|
-
|
|
61826
|
-
|
|
61827
|
-
|
|
61828
|
-
|
|
61829
|
-
|
|
61830
|
-
|
|
61831
|
-
|
|
61832
|
-
|
|
61833
|
-
|
|
61834
|
-
|
|
61835
|
-
|
|
61836
|
-
|
|
61837
|
-
if (
|
|
61838
|
-
|
|
61839
|
-
|
|
61840
|
-
|
|
61841
|
-
|
|
61842
|
-
}
|
|
61843
|
-
|
|
61844
|
-
|
|
61845
|
-
|
|
61846
|
-
|
|
61847
|
-
|
|
61848
|
-
|
|
61849
|
-
|
|
62706
|
+
async function devHandle(req) {
|
|
62707
|
+
{
|
|
62708
|
+
const u3 = new URL(req.url);
|
|
62709
|
+
if (u3.pathname === "/_tw/stream") {
|
|
62710
|
+
const { createSignalStream: createSignalStream2 } = await Promise.resolve().then(() => (init_signal_stream(), signal_stream_exports));
|
|
62711
|
+
return createSignalStream2(req, u3, devSigCfg ? { maxClients: devSigCfg.maxClients, maxHistory: devSigCfg.maxHistory } : void 0);
|
|
62712
|
+
}
|
|
62713
|
+
if (u3.pathname === "/_tw/signal" && req.method === "POST") {
|
|
62714
|
+
const { writeSignalFromClient: writeSignalFromClient2 } = await Promise.resolve().then(() => (init_signal_stream(), signal_stream_exports));
|
|
62715
|
+
return writeSignalFromClient2(req, { clientWrites: devSigCfg?.clientWrites });
|
|
62716
|
+
}
|
|
62717
|
+
}
|
|
62718
|
+
const reqUrl = new URL(req.url);
|
|
62719
|
+
for (const r of devRedirects) {
|
|
62720
|
+
if (!r?.from || !r?.to) continue;
|
|
62721
|
+
if (devPathMatch(r.from, reqUrl.pathname)) {
|
|
62722
|
+
const status = [301, 302, 307, 308].includes(r.status) ? r.status : 301;
|
|
62723
|
+
let loc = r.to.startsWith("http") ? r.to : new URL(r.to, req.url).toString();
|
|
62724
|
+
if (reqUrl.search && !loc.includes("?")) loc += reqUrl.search;
|
|
62725
|
+
return new Response(null, { status, headers: { Location: loc } });
|
|
62726
|
+
}
|
|
62727
|
+
}
|
|
62728
|
+
const mwHeaders = await runUserMiddleware(req);
|
|
62729
|
+
if (mwHeaders instanceof Response) return mwHeaders;
|
|
62730
|
+
const u = new URL(req.url);
|
|
62731
|
+
if (u.pathname === "/_tw/img" || u.pathname.startsWith("/_tw/img/")) {
|
|
62732
|
+
const imgRes = await devImageHandler?.handle(req);
|
|
62733
|
+
if (imgRes) return imgRes;
|
|
62734
|
+
}
|
|
62735
|
+
const response = await devFetchInner(req);
|
|
62736
|
+
if (mwHeaders && typeof mwHeaders === "object") {
|
|
62737
|
+
for (const key of Object.keys(mwHeaders)) {
|
|
62738
|
+
try {
|
|
62739
|
+
if (!response.headers.has(key)) response.headers.set(key, mwHeaders[key]);
|
|
62740
|
+
} catch {
|
|
61850
62741
|
}
|
|
61851
62742
|
}
|
|
61852
|
-
|
|
61853
|
-
|
|
61854
|
-
|
|
61855
|
-
|
|
61856
|
-
|
|
61857
|
-
|
|
61858
|
-
|
|
61859
|
-
|
|
62743
|
+
}
|
|
62744
|
+
const u2 = new URL(req.url);
|
|
62745
|
+
for (const h of devCfgHeaders) {
|
|
62746
|
+
if (h?.source && h?.headers && devPathMatch(h.source, u2.pathname)) {
|
|
62747
|
+
for (const key of Object.keys(h.headers)) {
|
|
62748
|
+
try {
|
|
62749
|
+
if (!response.headers.has(key)) response.headers.set(key, String(h.headers[key]).replace(/[\r\n]/g, ""));
|
|
62750
|
+
} catch {
|
|
61860
62751
|
}
|
|
61861
62752
|
}
|
|
61862
62753
|
}
|
|
61863
|
-
|
|
61864
|
-
|
|
61865
|
-
|
|
61866
|
-
|
|
61867
|
-
|
|
62754
|
+
}
|
|
62755
|
+
if (devSecurityHeaders2) {
|
|
62756
|
+
try {
|
|
62757
|
+
return devSecurityHeaders2.apply(response);
|
|
62758
|
+
} catch {
|
|
61868
62759
|
}
|
|
61869
|
-
return response;
|
|
61870
62760
|
}
|
|
61871
|
-
|
|
62761
|
+
return response;
|
|
62762
|
+
}
|
|
62763
|
+
if (typeof Bun !== "undefined" && typeof Bun.serve === "function") {
|
|
62764
|
+
Bun.serve({ port, hostname: host, fetch: devHandle });
|
|
62765
|
+
} else {
|
|
62766
|
+
const { createServer } = await import("node:http");
|
|
62767
|
+
const srv = createServer(async (nreq, nres) => {
|
|
62768
|
+
try {
|
|
62769
|
+
const hostHdr = String(nreq.headers.host ?? `${host}:${port}`);
|
|
62770
|
+
const chunks = [];
|
|
62771
|
+
for await (const c of nreq) chunks.push(Buffer.from(c));
|
|
62772
|
+
const raw = nreq.method === "GET" || nreq.method === "HEAD" ? void 0 : Buffer.concat(chunks);
|
|
62773
|
+
const request = new Request(`http://${hostHdr}${nreq.url}`, {
|
|
62774
|
+
method: nreq.method,
|
|
62775
|
+
headers: nreq.headers,
|
|
62776
|
+
body: raw && raw.length > 0 ? raw : void 0
|
|
62777
|
+
});
|
|
62778
|
+
const resp = await devHandle(request);
|
|
62779
|
+
const out = Buffer.from(await resp.arrayBuffer());
|
|
62780
|
+
const headers = {};
|
|
62781
|
+
resp.headers.forEach((v, k) => {
|
|
62782
|
+
headers[k] = v;
|
|
62783
|
+
});
|
|
62784
|
+
nres.writeHead(resp.status, headers);
|
|
62785
|
+
nres.end(out);
|
|
62786
|
+
} catch (e) {
|
|
62787
|
+
nres.writeHead(500, { "Content-Type": "text/plain" });
|
|
62788
|
+
nres.end("Internal dev server error: " + (e?.message ?? e));
|
|
62789
|
+
}
|
|
62790
|
+
});
|
|
62791
|
+
await new Promise((r) => srv.listen(port, host, () => r()));
|
|
62792
|
+
console.log(" [tw] runtime: node (Bun not found \u2014 using node:http)");
|
|
62793
|
+
}
|
|
62794
|
+
try {
|
|
62795
|
+
const { watch: watch2 } = await import("node:fs");
|
|
62796
|
+
const seen = /* @__PURE__ */ new Set();
|
|
62797
|
+
watch2(rootDir, { recursive: true }, (_t, f) => {
|
|
62798
|
+
const name = String(f);
|
|
62799
|
+
if (name.includes("node_modules") || name.includes(".git") || name.split("/").includes(".tw")) return;
|
|
62800
|
+
if (seen.has(name)) return;
|
|
62801
|
+
seen.add(name);
|
|
62802
|
+
const t2 = setTimeout(() => seen.delete(name), 400);
|
|
62803
|
+
if (typeof t2?.unref === "function") t2.unref();
|
|
62804
|
+
console.log(` [tw] ${name} changed -- reload the browser`);
|
|
62805
|
+
});
|
|
62806
|
+
} catch {
|
|
62807
|
+
}
|
|
61872
62808
|
console.log(" Server running! Open http://" + host + ":" + port + " in your browser.");
|
|
61873
62809
|
console.log(" Press Ctrl+C to stop.\n");
|
|
61874
62810
|
}
|
|
@@ -61881,6 +62817,25 @@ import { join as join22 } from "node:path";
|
|
|
61881
62817
|
import { existsSync as existsSync18 } from "node:fs";
|
|
61882
62818
|
async function serveCommand() {
|
|
61883
62819
|
const args = process.argv.slice(3);
|
|
62820
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
62821
|
+
console.log(`
|
|
62822
|
+
tw serve \u2014 start the production server
|
|
62823
|
+
|
|
62824
|
+
Usage:
|
|
62825
|
+
tw serve [--port <number>] [--host <address>]
|
|
62826
|
+
|
|
62827
|
+
Options:
|
|
62828
|
+
--port <number> port to listen on (default: 8000; tw.config.ts
|
|
62829
|
+
server.port and the PORT env var also work,
|
|
62830
|
+
in that order of precedence)
|
|
62831
|
+
--host <address> bind address (default: 0.0.0.0)
|
|
62832
|
+
-h, --help show this help
|
|
62833
|
+
|
|
62834
|
+
Requires a build first (tw build); serves .tw/ output with SSR,
|
|
62835
|
+
middleware and API routes.
|
|
62836
|
+
`);
|
|
62837
|
+
process.exit(0);
|
|
62838
|
+
}
|
|
61884
62839
|
let port = null;
|
|
61885
62840
|
let host = "0.0.0.0";
|
|
61886
62841
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -61895,8 +62850,8 @@ async function serveCommand() {
|
|
|
61895
62850
|
const envPath = join22(rootDir, ".env");
|
|
61896
62851
|
if (existsSync18(envPath)) {
|
|
61897
62852
|
try {
|
|
61898
|
-
const { readFileSync:
|
|
61899
|
-
for (const line of
|
|
62853
|
+
const { readFileSync: readFileSync20 } = await import("node:fs");
|
|
62854
|
+
for (const line of readFileSync20(envPath, "utf-8").split("\n")) {
|
|
61900
62855
|
const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/.exec(line);
|
|
61901
62856
|
if (m && !(m[1] in process.env)) {
|
|
61902
62857
|
process.env[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
@@ -61942,7 +62897,7 @@ async function serveCommand() {
|
|
|
61942
62897
|
staticDir: outDir,
|
|
61943
62898
|
config,
|
|
61944
62899
|
...config?.rateLimit ? { rateLimit: config.rateLimit } : {},
|
|
61945
|
-
...
|
|
62900
|
+
...config?.compression !== void 0 || config?.server?.compression !== void 0 ? { compression: config?.compression ?? config?.server?.compression } : {},
|
|
61946
62901
|
...pluginManager && pluginManager.size() > 0 ? { plugins: pluginManager } : {},
|
|
61947
62902
|
...config?.images ? { images: config.images } : {},
|
|
61948
62903
|
...config?.security ? { security: config.security } : {}
|
|
@@ -62221,7 +63176,7 @@ async function adapterCommand() {
|
|
|
62221
63176
|
// tw/commands/test.ts
|
|
62222
63177
|
import { join as join27 } from "node:path";
|
|
62223
63178
|
import { createRequire as _cr } from "node:module";
|
|
62224
|
-
import { existsSync as existsSync22, readdirSync as readdirSync9, statSync as statSync9 } from "node:fs";
|
|
63179
|
+
import { existsSync as existsSync22, readdirSync as readdirSync9, statSync as statSync9, readFileSync as readFileSync15 } from "node:fs";
|
|
62225
63180
|
var _require = _cr(import.meta.url);
|
|
62226
63181
|
function collectTests(dir, out) {
|
|
62227
63182
|
if (!existsSync22(dir)) return;
|
|
@@ -62277,6 +63232,17 @@ async function testCommand() {
|
|
|
62277
63232
|
}
|
|
62278
63233
|
}
|
|
62279
63234
|
console.log("\n tw test -- running " + files.length + " test file" + (files.length === 1 ? "" : "s") + "\n");
|
|
63235
|
+
for (const f of files) {
|
|
63236
|
+
try {
|
|
63237
|
+
const src = readFileSync15(f, "utf-8");
|
|
63238
|
+
const hasBunTest = src.includes("bun:test");
|
|
63239
|
+
const hasTestCall = /\b(test|it|describe)\s*\(/.test(src);
|
|
63240
|
+
if (!hasBunTest && !hasTestCall) {
|
|
63241
|
+
console.log(" \x1B[33m! " + f.replace(rootDir + "/", "") + ' defined no tests -- only bun:test format runs (import { test, expect } from "bun:test")\x1B[0m');
|
|
63242
|
+
}
|
|
63243
|
+
} catch {
|
|
63244
|
+
}
|
|
63245
|
+
}
|
|
62280
63246
|
if (files.length === 0) {
|
|
62281
63247
|
console.log(" No tests found. Create tests/your-name.test.ts:");
|
|
62282
63248
|
console.log(`
|
|
@@ -62309,8 +63275,8 @@ async function testCommand() {
|
|
|
62309
63275
|
|
|
62310
63276
|
// tw/commands/check.ts
|
|
62311
63277
|
init_tw();
|
|
62312
|
-
import { join as join28 } from "node:path";
|
|
62313
|
-
import { existsSync as existsSync23, readFileSync as
|
|
63278
|
+
import { join as join28, dirname as dirname8 } from "node:path";
|
|
63279
|
+
import { existsSync as existsSync23, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync10 } from "node:fs";
|
|
62314
63280
|
async function checkCommand() {
|
|
62315
63281
|
const args = process.argv.slice(3);
|
|
62316
63282
|
const rootDir = process.cwd();
|
|
@@ -62332,7 +63298,7 @@ async function checkCommand() {
|
|
|
62332
63298
|
for (const filePath of twFiles) {
|
|
62333
63299
|
const relativePath = filePath.replace(rootDir + "/", "");
|
|
62334
63300
|
try {
|
|
62335
|
-
const source =
|
|
63301
|
+
const source = readFileSync16(filePath, "utf-8");
|
|
62336
63302
|
const result = await compile2(source, {
|
|
62337
63303
|
filePath,
|
|
62338
63304
|
optimize: false,
|
|
@@ -62343,6 +63309,42 @@ async function checkCommand() {
|
|
|
62343
63309
|
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
62344
63310
|
const warnings = diagnostics.filter((d) => d.severity === "warning");
|
|
62345
63311
|
const info = diagnostics.filter((d) => d.severity === "info");
|
|
63312
|
+
const checkSource = readFileSync16(filePath, "utf-8");
|
|
63313
|
+
const usedTags = /* @__PURE__ */ new Set();
|
|
63314
|
+
const collectUsed = (n) => {
|
|
63315
|
+
if (!n) return;
|
|
63316
|
+
if (n.type === "Element" || n.type === "Component") {
|
|
63317
|
+
const t2 = n.tag ?? n.name;
|
|
63318
|
+
if (typeof t2 === "string" && t2[0] === t2[0].toUpperCase()) usedTags.add(t2);
|
|
63319
|
+
}
|
|
63320
|
+
for (const c of n.children ?? []) collectUsed(c);
|
|
63321
|
+
if (n.type === "If" || n.type === "For" || n.type === "While") for (const c of n.body ?? []) collectUsed(c);
|
|
63322
|
+
};
|
|
63323
|
+
if (result.ast) {
|
|
63324
|
+
for (const n of result.ast.body ?? []) collectUsed(n);
|
|
63325
|
+
}
|
|
63326
|
+
const SAFE_TAGS = /* @__PURE__ */ new Set(["Link", "Suspense", "RouterLink", "Image", "optImage"]);
|
|
63327
|
+
const importedNames = /* @__PURE__ */ new Set();
|
|
63328
|
+
for (const im of checkSource.matchAll(/import\s+([A-Za-z_]\w*)\s+from/g)) importedNames.add(im[1]);
|
|
63329
|
+
for (const im of checkSource.matchAll(/import\s*\{([^}]+)\}/g)) {
|
|
63330
|
+
for (const part of im[1].split(",")) {
|
|
63331
|
+
const nm = part.split(" as ").pop().trim();
|
|
63332
|
+
if (nm) importedNames.add(nm);
|
|
63333
|
+
}
|
|
63334
|
+
}
|
|
63335
|
+
const unknownComponents = [];
|
|
63336
|
+
for (const tag of usedTags) {
|
|
63337
|
+
if (SAFE_TAGS.has(tag) || importedNames.has(tag)) continue;
|
|
63338
|
+
if (findComponentUp(dirname8(filePath), tag)) continue;
|
|
63339
|
+
unknownComponents.push(tag);
|
|
63340
|
+
}
|
|
63341
|
+
if (unknownComponents.length > 0) {
|
|
63342
|
+
console.debug(` ? ${relativePath} -- ${unknownComponents.length} unknown component(s)`);
|
|
63343
|
+
for (const t2 of unknownComponents) {
|
|
63344
|
+
console.debug(` ? TW098 unknown component <${t2}> -- no import resolves it and no components/${t2}.tw exists`);
|
|
63345
|
+
}
|
|
63346
|
+
totalWarnings += unknownComponents.length;
|
|
63347
|
+
}
|
|
62346
63348
|
totalErrors += errors.length;
|
|
62347
63349
|
totalWarnings += warnings.length;
|
|
62348
63350
|
totalInfo += info.length;
|
|
@@ -62365,7 +63367,7 @@ async function checkCommand() {
|
|
|
62365
63367
|
console.debug(" Compiler not found, running basic checks...\n");
|
|
62366
63368
|
for (const filePath of twFiles) {
|
|
62367
63369
|
const relativePath = filePath.replace(rootDir + "/", "");
|
|
62368
|
-
const source =
|
|
63370
|
+
const source = readFileSync16(filePath, "utf-8");
|
|
62369
63371
|
const maskedSource = maskSourceStringsAndComments(source);
|
|
62370
63372
|
const openBraces = (maskedSource.match(/\{/g) || []).length;
|
|
62371
63373
|
const closeBraces = (maskedSource.match(/\}/g) || []).length;
|
|
@@ -62379,13 +63381,41 @@ async function checkCommand() {
|
|
|
62379
63381
|
}
|
|
62380
63382
|
console.log(`
|
|
62381
63383
|
Summary: ${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info`);
|
|
63384
|
+
const argv = process.argv.slice(2);
|
|
63385
|
+
let maxWarnings = null;
|
|
63386
|
+
if (argv.includes("--strict")) maxWarnings = 0;
|
|
63387
|
+
const mwIdx = argv.indexOf("--max-warnings");
|
|
63388
|
+
if (mwIdx !== -1 && argv[mwIdx + 1] !== void 0 && /^\d+$/.test(argv[mwIdx + 1])) {
|
|
63389
|
+
maxWarnings = Number(argv[mwIdx + 1]);
|
|
63390
|
+
} else {
|
|
63391
|
+
const eq = argv.find((a) => a.startsWith("--max-warnings="));
|
|
63392
|
+
if (eq && /^\d+$/.test(eq.split("=")[1])) maxWarnings = Number(eq.split("=")[1]);
|
|
63393
|
+
}
|
|
62382
63394
|
if (totalErrors > 0) {
|
|
62383
63395
|
console.log(" Status: FAILED\n");
|
|
62384
63396
|
process.exit(1);
|
|
63397
|
+
} else if (maxWarnings !== null && totalWarnings > maxWarnings) {
|
|
63398
|
+
console.log(` Status: FAILED -- ${totalWarnings} warnings exceed --max-warnings=${maxWarnings}
|
|
63399
|
+
`);
|
|
63400
|
+
process.exit(1);
|
|
62385
63401
|
} else {
|
|
62386
63402
|
console.log(" Status: PASSED\n");
|
|
62387
63403
|
}
|
|
62388
63404
|
}
|
|
63405
|
+
function findComponentUp(dir, name) {
|
|
63406
|
+
let cur = dir;
|
|
63407
|
+
for (let i = 0; i < 12 && cur && cur !== "/"; i++) {
|
|
63408
|
+
try {
|
|
63409
|
+
const candidate = join28(cur, "components", name + ".tw");
|
|
63410
|
+
if (existsSync23(candidate)) return true;
|
|
63411
|
+
} catch {
|
|
63412
|
+
}
|
|
63413
|
+
const parent = dirname8(cur);
|
|
63414
|
+
if (parent === cur) break;
|
|
63415
|
+
cur = parent;
|
|
63416
|
+
}
|
|
63417
|
+
return false;
|
|
63418
|
+
}
|
|
62389
63419
|
function collectFiles2(dir, ext, results) {
|
|
62390
63420
|
const entries = readdirSync10(dir);
|
|
62391
63421
|
for (const entry of entries) {
|
|
@@ -62402,10 +63432,10 @@ function collectFiles2(dir, ext, results) {
|
|
|
62402
63432
|
// tw/commands/plugin.ts
|
|
62403
63433
|
init_tw();
|
|
62404
63434
|
import { join as join29 } from "node:path";
|
|
62405
|
-
import { existsSync as existsSync24, readFileSync as
|
|
63435
|
+
import { existsSync as existsSync24, readFileSync as readFileSync17, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, readdirSync as readdirSync11 } from "node:fs";
|
|
62406
63436
|
function readConfigPlugins(cfgPath) {
|
|
62407
63437
|
if (!existsSync24(cfgPath)) return [];
|
|
62408
|
-
const src = stripCommentsStringAware(
|
|
63438
|
+
const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
|
|
62409
63439
|
const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
|
|
62410
63440
|
if (!m) return [];
|
|
62411
63441
|
const out = [];
|
|
@@ -62424,7 +63454,7 @@ function addConfigPlugin(cfgPath, name) {
|
|
|
62424
63454
|
`);
|
|
62425
63455
|
return true;
|
|
62426
63456
|
}
|
|
62427
|
-
const src = stripCommentsStringAware(
|
|
63457
|
+
const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
|
|
62428
63458
|
if (new RegExp(`["'\`]${name}["'\`]`).test(src)) return false;
|
|
62429
63459
|
const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
|
|
62430
63460
|
if (m) {
|
|
@@ -62441,7 +63471,7 @@ function addConfigPlugin(cfgPath, name) {
|
|
|
62441
63471
|
}
|
|
62442
63472
|
function removeConfigPlugin(cfgPath, name) {
|
|
62443
63473
|
if (!existsSync24(cfgPath)) return false;
|
|
62444
|
-
const src = stripCommentsStringAware(
|
|
63474
|
+
const src = stripCommentsStringAware(readFileSync17(cfgPath, "utf-8"));
|
|
62445
63475
|
const m = /plugins\s*:\s*\[([^\]]*)\]/s.exec(src);
|
|
62446
63476
|
if (!m) return false;
|
|
62447
63477
|
const kept = m[1].split(",").map((e) => e.trim()).filter((e) => e && e !== `"${name}"` && e !== `'${name}'` && e !== "`" + name + "`");
|
|
@@ -62645,13 +63675,13 @@ async function lspCommand() {
|
|
|
62645
63675
|
}
|
|
62646
63676
|
|
|
62647
63677
|
// tw/help.ts
|
|
62648
|
-
import { readFileSync as
|
|
63678
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
62649
63679
|
import { join as join31 } from "node:path";
|
|
62650
63680
|
function cliVersion() {
|
|
62651
63681
|
try {
|
|
62652
63682
|
const bd = globalThis.__TW_BUNDLE_DIR;
|
|
62653
63683
|
const pj = bd ? join31(bd, "..", "package.json") : new URL("../../package.json", import.meta.url).pathname;
|
|
62654
|
-
return JSON.parse(
|
|
63684
|
+
return JSON.parse(readFileSync18(pj, "utf-8")).version ?? "0.0.0";
|
|
62655
63685
|
} catch {
|
|
62656
63686
|
return "0.0.0";
|
|
62657
63687
|
}
|
|
@@ -62724,13 +63754,13 @@ var helpText = `
|
|
|
62724
63754
|
|
|
62725
63755
|
// tw/commands/index.ts
|
|
62726
63756
|
await init_build();
|
|
62727
|
-
import { readFileSync as
|
|
63757
|
+
import { readFileSync as readFileSync19 } from "node:fs";
|
|
62728
63758
|
import { join as join32 } from "node:path";
|
|
62729
63759
|
var VERSION = (() => {
|
|
62730
63760
|
try {
|
|
62731
63761
|
const bd = globalThis.__TW_BUNDLE_DIR;
|
|
62732
63762
|
const pj = bd ? join32(bd, "..", "package.json") : new URL("../../package.json", import.meta.url).pathname;
|
|
62733
|
-
return JSON.parse(
|
|
63763
|
+
return JSON.parse(readFileSync19(pj, "utf-8")).version ?? "0.0.0";
|
|
62734
63764
|
} catch {
|
|
62735
63765
|
return "0.0.0";
|
|
62736
63766
|
}
|