@vesk/vesk-cli 0.2.50-canary.10 → 0.2.50-canary.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +432 -54
  2. package/package.json +5 -5
package/dist/cli.js CHANGED
@@ -147,7 +147,10 @@ var init_errors = __esm({
147
147
  suggestions;
148
148
  nextSteps;
149
149
  tip;
150
+ /** The V-diagnostic code, e.g. `V0501`. */
150
151
  code;
152
+ /** The rendered code frame. Used to be written into `code`, which silently replaced the V-code. */
153
+ frame;
151
154
  constructor(message, opts = {}) {
152
155
  super(message);
153
156
  this.name = "VeskError";
@@ -157,7 +160,8 @@ var init_errors = __esm({
157
160
  this.suggestions = opts.suggestions || [];
158
161
  this.nextSteps = opts.nextSteps || [];
159
162
  this.tip = opts.tip || "";
160
- if (opts.code !== void 0) this.code = opts.code;
163
+ if (opts.code !== void 0 && !opts.code.includes("\n")) this.code = opts.code;
164
+ if (opts.frame !== void 0) this.frame = opts.frame;
161
165
  }
162
166
  static notFound(name, candidates = [], context = {}) {
163
167
  const allCandidates = [.../* @__PURE__ */ new Set([...candidates, ...VESK_BUILTINS])];
@@ -236,6 +240,84 @@ var init_errors = __esm({
236
240
  }
237
241
  );
238
242
  }
243
+ /**
244
+ * A browser-only global used in code the SERVER evaluates.
245
+ *
246
+ * On the server these are either absent (`window is not defined`) or, worse,
247
+ * silently present-but-wrong (`navigator` exists in modern Node, so
248
+ * `navigator.onLine` is `undefined` and a page renders as if it were offline).
249
+ * A raw `ReferenceError` at request time is the worst possible report: it
250
+ * names neither the component nor the fix. This points at the line.
251
+ */
252
+ static ssrUnsafeGlobal(name, context = {}) {
253
+ return new _VeskError(
254
+ `\`${name}\` is browser-only, and this component body runs during SSR.`,
255
+ {
256
+ ...context,
257
+ code: context.code || "V0501",
258
+ suggestions: [
259
+ `Wrap the part that needs the DOM in a {#client} block.`,
260
+ `Move the code into an event handler \u2014 handlers never run on the server.`,
261
+ `Do the setup in on_destroy(), which the compiler only emits on the client.`
262
+ ],
263
+ nextSteps: [
264
+ `component ${context.inComponent ?? "this component"}: wrap the \`${name}\` access in {#client}...{/client}.`,
265
+ `Or guard it: if (typeof ${name} !== 'undefined') { ... }`
266
+ ],
267
+ tip: "SSR renders this component before the client exists. Reading a browser global here either throws during the request or (for `navigator`) silently returns undefined."
268
+ }
269
+ );
270
+ }
271
+ /**
272
+ * The second name from `track()` called as a function.
273
+ *
274
+ * `const &[count, setCount] = track(0)` returns a CELL, not a setter function, so
275
+ * `setCount(1)` throws `setCount is not a function` from generated code with
276
+ * no line of user code in the message. The cell is the writer: `.set(v)`.
277
+ */
278
+ static cellCalledAsFunction(name, context = {}) {
279
+ return new _VeskError(
280
+ `\`${name}\` is a cell, not a function \u2014 cells are written with \`.set()\`.`,
281
+ {
282
+ ...context,
283
+ code: context.code || "V0502",
284
+ suggestions: [
285
+ `Use \`${name}.set(...)\` instead of \`${name}(...)\`.`,
286
+ `Or use the tracked spelling inside an event handler: set(${context.value ?? "value"}, ...)`
287
+ ],
288
+ nextSteps: [
289
+ `const &[${context.value ?? "value"}, ${name}] = track(0) \u2192 \`${name}.set(v)\``,
290
+ "The value read in markup is the first name; the second is the cell object."
291
+ ],
292
+ tip: `\`track()\` returns [value, cell]. \`${name}\` is the cell half.`
293
+ }
294
+ );
295
+ }
296
+ /**
297
+ * `set(value, …)` used outside a rewritten expression.
298
+ *
299
+ * The compiler rewrites `set(count, v)` to `countCell.set(v)` inside JSX and
300
+ * event handlers — but a bare statement in the component body is NOT
301
+ * rewritten, so the runtime `set` receives the value and throws
302
+ * "Cannot read properties of undefined". Pass the CELL, not the value.
303
+ */
304
+ static setWithValueName(name, context = {}) {
305
+ return new _VeskError(
306
+ `\`set(${name}, \u2026)\` needs the CELL, and \`${name}\` is the value half of track().`,
307
+ {
308
+ ...context,
309
+ code: context.code || "V0503",
310
+ suggestions: [
311
+ `Use \`set(${context.cell ?? "cell"}, \u2026)\` or \`${context.cell ?? "cell"}.set(\u2026)\`.`,
312
+ `Or move the set() into an event handler, where the compiler rewrites it for you.`
313
+ ],
314
+ nextSteps: [
315
+ `const &[${name}, ${context.cell ?? "cell"}] = track(0) \u2192 \`${context.cell ?? "cell"}.set(v)\``
316
+ ],
317
+ tip: "set(cell, value) is only rewritten inside JSX and handlers; in a body statement it reaches the runtime untouched."
318
+ }
319
+ );
320
+ }
239
321
  static serverBlockInClient(compName, context = {}) {
240
322
  return new _VeskError(
241
323
  `{#server} block found in client island "${compName}". Client islands render on both server and client, so {#server} blocks have no effect.`,
@@ -21282,7 +21364,7 @@ function validateBlocks(compName, isClient, body, file, source) {
21282
21364
  const { line, column } = offsetToLineCol2(source, pos);
21283
21365
  opts.line = line;
21284
21366
  opts.column = column;
21285
- opts.code = codeFrame(source, line, column);
21367
+ opts.frame = codeFrame(source, line, column);
21286
21368
  }
21287
21369
  throw VeskError.serverBlockInClient(compName, opts);
21288
21370
  }
@@ -21294,7 +21376,7 @@ function validateBlocks(compName, isClient, body, file, source) {
21294
21376
  const { line, column } = offsetToLineCol2(source, pos);
21295
21377
  opts.line = line;
21296
21378
  opts.column = column;
21297
- opts.code = codeFrame(source, line, column);
21379
+ opts.frame = codeFrame(source, line, column);
21298
21380
  }
21299
21381
  throw VeskError.clientBlockInServer(compName, opts);
21300
21382
  }
@@ -25890,8 +25972,8 @@ function isPublicMarkdownPath(v) {
25890
25972
  const c = s.charCodeAt(i);
25891
25973
  if (c === 63 || c === 35 || c === 92) return false;
25892
25974
  }
25893
- const lower = s.toLowerCase();
25894
- return stringEndsWith(lower, ".md") || stringEndsWith(lower, ".markdown");
25975
+ const lower2 = s.toLowerCase();
25976
+ return stringEndsWith(lower2, ".md") || stringEndsWith(lower2, ".markdown");
25895
25977
  }
25896
25978
  function getMdPathCell(path) {
25897
25979
  let cell = mdPathCells.get(path);
@@ -26563,9 +26645,9 @@ function toDomNode(value) {
26563
26645
  }
26564
26646
  if (typeof value !== "string") return value;
26565
26647
  if (value === "") return document.createDocumentFragment();
26566
- const template = document.createElement("template");
26567
- template.innerHTML = value;
26568
- const { content } = template;
26648
+ const template2 = document.createElement("template");
26649
+ template2.innerHTML = value;
26650
+ const { content } = template2;
26569
26651
  if (content.childNodes.length === 1) {
26570
26652
  const only = content.firstChild;
26571
26653
  content.removeChild(only);
@@ -26758,7 +26840,7 @@ function resolveTitle(meta, child) {
26758
26840
  }
26759
26841
  function mergeMetadata(...sources) {
26760
26842
  const out = {};
26761
- let template;
26843
+ let template2;
26762
26844
  let sawTemplate = false;
26763
26845
  for (const source of sources) {
26764
26846
  if (!source) continue;
@@ -26771,7 +26853,7 @@ function mergeMetadata(...sources) {
26771
26853
  if (key === "title") {
26772
26854
  if (value && typeof value === "object" && value.template) {
26773
26855
  sawTemplate = true;
26774
- template = value.template || template;
26856
+ template2 = value.template || template2;
26775
26857
  }
26776
26858
  out.title = value;
26777
26859
  continue;
@@ -26783,9 +26865,9 @@ function mergeMetadata(...sources) {
26783
26865
  const own = out.title;
26784
26866
  if (typeof own === "string") out.title = { absolute: own };
26785
26867
  else if (own && typeof own === "object" && own.template) {
26786
- out.title = { default: own.default, template: own.template || template };
26868
+ out.title = { default: own.default, template: own.template || template2 };
26787
26869
  } else if (own && typeof own === "object") {
26788
- out.title = { ...own, template };
26870
+ out.title = { ...own, template: template2 };
26789
26871
  }
26790
26872
  }
26791
26873
  return out;
@@ -29289,8 +29371,8 @@ function looksLikeMarkdownPath(value) {
29289
29371
  const v = value.trim();
29290
29372
  if (v.length === 0 || v.length > 4096) return false;
29291
29373
  if (v.includes("\n") || v.includes("\r")) return false;
29292
- const lower = v.toLowerCase();
29293
- return lower.endsWith(MD_EXT_MARKER) || lower.endsWith(".markdown");
29374
+ const lower2 = v.toLowerCase();
29375
+ return lower2.endsWith(MD_EXT_MARKER) || lower2.endsWith(".markdown");
29294
29376
  }
29295
29377
  function isRelativeSpecifier(v) {
29296
29378
  return v.startsWith("./") || v.startsWith("../");
@@ -31863,7 +31945,202 @@ var init_server_head = __esm({
31863
31945
  }
31864
31946
  });
31865
31947
 
31948
+ // ../compiler/src/server-jsx-lowering.ts
31949
+ function containsJsx2(node, depth = 0) {
31950
+ if (!node || typeof node !== "object" || depth > 40) return false;
31951
+ if (Array.isArray(node)) {
31952
+ for (const item of node) if (containsJsx2(item, depth + 1)) return true;
31953
+ return false;
31954
+ }
31955
+ const type = node.type;
31956
+ if (type === "JSXElement" || type === "JSXFragment") return true;
31957
+ for (const key of Object.keys(node)) {
31958
+ const value = node[key];
31959
+ if (value && typeof value === "object" && containsJsx2(value, depth + 1)) return true;
31960
+ }
31961
+ return false;
31962
+ }
31963
+ function normalizeJsxText(value) {
31964
+ const lines = value.split(/\r\n|\n|\r/);
31965
+ let lastNonEmpty = -1;
31966
+ for (let i = 0; i < lines.length; i++) {
31967
+ if (/[^ \t]/.test(lines[i])) lastNonEmpty = i;
31968
+ }
31969
+ let out = "";
31970
+ for (let i = 0; i < lines.length; i++) {
31971
+ let line = lines[i].replace(/\t/g, " ");
31972
+ if (i !== 0) line = line.replace(/^ +/, "");
31973
+ if (i !== lines.length - 1) line = line.replace(/ +$/, "");
31974
+ if (line) {
31975
+ if (i !== lastNonEmpty) line += " ";
31976
+ out += line;
31977
+ }
31978
+ }
31979
+ return out;
31980
+ }
31981
+ function lower(node, ctx2) {
31982
+ if (Array.isArray(node)) return node.map((n2) => lower(n2, ctx2));
31983
+ if (!node || typeof node !== "object") return node;
31984
+ const n = node;
31985
+ if (n.type === "JSXElement") {
31986
+ const opening = n.openingElement;
31987
+ const name = jsxTagName(opening.name);
31988
+ const attrs = lowerAttributes(opening.attributes, ctx2);
31989
+ const children = lowerChildren(n.children, ctx2);
31990
+ const statics = [`<${name}`, ...attrs.statics.slice(1)];
31991
+ statics[statics.length - 1] += ">";
31992
+ const quasis = [...statics, `</${name}>`];
31993
+ const expressions = [...attrs.exprs, ...children];
31994
+ return template(quasis, expressions);
31995
+ }
31996
+ if (n.type === "JSXFragment") {
31997
+ return template(["", ""], lowerChildren(n.children, ctx2));
31998
+ }
31999
+ const out = {};
32000
+ for (const key of Object.keys(n)) {
32001
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") {
32002
+ out[key] = n[key];
32003
+ continue;
32004
+ }
32005
+ const value = n[key];
32006
+ out[key] = value && typeof value === "object" ? lower(value, ctx2) : value;
32007
+ }
32008
+ return out;
32009
+ }
32010
+ function jsxTagName(nameNode) {
32011
+ const n = nameNode;
32012
+ if (!n) return "div";
32013
+ if (n.type === "JSXIdentifier") return String(n.name);
32014
+ if (n.type === "JSXMemberExpression") {
32015
+ const object = jsxTagName(n.object);
32016
+ const property = n.property;
32017
+ return `${object}.${String(property?.name)}`;
32018
+ }
32019
+ if (n.type === "JSXNamespacedName") {
32020
+ const ns = n.namespace;
32021
+ const name = n.name;
32022
+ return `${String(ns?.name)}:${String(name?.name)}`;
32023
+ }
32024
+ return "div";
32025
+ }
32026
+ function lowerAttributes(attrs, ctx2) {
32027
+ const statics = [""];
32028
+ const exprs = [];
32029
+ const push = (text, expr) => {
32030
+ if (expr === void 0) {
32031
+ statics[statics.length - 1] += text;
32032
+ return;
32033
+ }
32034
+ statics[statics.length - 1] += text;
32035
+ statics.push("");
32036
+ exprs.push(expr);
32037
+ };
32038
+ for (const attr of attrs || []) {
32039
+ const a = attr;
32040
+ if (a.type === "JSXSpreadAttribute") {
32041
+ console.error("[vesk] a spread attribute in a JSX expression is not supported yet (dropped):", String(a.argument ?? "").slice(0, 60));
32042
+ continue;
32043
+ }
32044
+ const name = jsxAttrName(a.name);
32045
+ const value = a.value;
32046
+ if (value === null || value === void 0) {
32047
+ push(` ${name}`);
32048
+ continue;
32049
+ }
32050
+ if (value.type === "StringLiteral") {
32051
+ push(` ${name}="${escapeAttrText(String(value.value))}"`);
32052
+ continue;
32053
+ }
32054
+ if (value.type === "JSXExpressionContainer") {
32055
+ const inner = value.expression;
32056
+ if (!inner || inner.type === "JSXEmptyExpression") {
32057
+ push(` ${name}="true"`);
32058
+ continue;
32059
+ }
32060
+ push(` ${name}="`, callNode(ATTR, [literal3(name), coerceString(lower(inner, ctx2))]));
32061
+ push('"');
32062
+ continue;
32063
+ }
32064
+ push(` ${name}="`, callNode(ATTR, [literal3(name), coerceString(lower(value, ctx2))]));
32065
+ push('"');
32066
+ }
32067
+ return { statics, exprs };
32068
+ }
32069
+ function jsxAttrName(nameNode) {
32070
+ const n = nameNode;
32071
+ if (!n) return "data";
32072
+ if (n.type === "JSXIdentifier") return String(n.name);
32073
+ return jsxTagName(n);
32074
+ }
32075
+ function lowerChildren(children, ctx2) {
32076
+ const out = [];
32077
+ for (const child of children || []) {
32078
+ const c = child;
32079
+ if (c.type === "JSXText") {
32080
+ const text = normalizeJsxText(String(c.value ?? ""));
32081
+ if (text) out.push(literal3(text));
32082
+ continue;
32083
+ }
32084
+ if (c.type === "JSXExpressionContainer") {
32085
+ const inner = c.expression;
32086
+ if (!inner || inner.type === "JSXEmptyExpression") continue;
32087
+ const lowered2 = lower(inner, ctx2);
32088
+ out.push(coerceString(lowered2));
32089
+ continue;
32090
+ }
32091
+ const lowered = lower(c, ctx2);
32092
+ out.push(coerceString(lowered));
32093
+ }
32094
+ return out;
32095
+ }
32096
+ function coerceString(node) {
32097
+ if (!node || typeof node !== "object") return callNode(ESCAPE, [literal3(String(node))]);
32098
+ const n = node;
32099
+ if (n.type === "TemplateLiteral") return n;
32100
+ if (n.type === "Literal" || n.type === "StringLiteral") return callNode(ESCAPE, [node]);
32101
+ return callNode(ESCAPE, [callNode("String", [node])]);
32102
+ }
32103
+ function template(statics, expressions) {
32104
+ const quasis = statics.map((raw2, i) => ({
32105
+ type: "TemplateElement",
32106
+ value: { raw: raw2 },
32107
+ tail: i === statics.length - 1
32108
+ }));
32109
+ return { type: "TemplateLiteral", quasis, expressions };
32110
+ }
32111
+ function literal3(value) {
32112
+ return { type: "Literal", value };
32113
+ }
32114
+ function callNode(callee, args2) {
32115
+ return {
32116
+ type: "CallExpression",
32117
+ callee: { type: "Identifier", name: callee },
32118
+ arguments: args2,
32119
+ optional: false
32120
+ };
32121
+ }
32122
+ function escapeAttrText(value) {
32123
+ return value.split("&").join("&amp;").split("<").join("&lt;").split(">").join("&gt;").split('"').join("&quot;");
32124
+ }
32125
+ function lowerJsxInExpression(ast) {
32126
+ if (!containsJsx2(ast)) return ast;
32127
+ return lower(ast, { track: false });
32128
+ }
32129
+ var ESCAPE, ATTR;
32130
+ var init_server_jsx_lowering = __esm({
32131
+ "../compiler/src/server-jsx-lowering.ts"() {
32132
+ "use strict";
32133
+ ESCAPE = "__escape";
32134
+ ATTR = "__attr";
32135
+ }
32136
+ });
32137
+
31866
32138
  // ../compiler/src/server-jsgen.ts
32139
+ import { print as print6 } from "esrap";
32140
+ import tsPrinter from "esrap/languages/ts";
32141
+ function printLoweredTs(ast) {
32142
+ return print6(ast, tsPrinter()).code;
32143
+ }
31867
32144
  function sanitizeMarkerName(name) {
31868
32145
  return String(name).split("--").join("-").split("<").join("").split(">").join("");
31869
32146
  }
@@ -31914,7 +32191,19 @@ function irNodeToJS(node, importedNames, isAsync = false, tracked2) {
31914
32191
  `})();`
31915
32192
  ].join("\n");
31916
32193
  }
31917
- if (node instanceof RuntimeStatement) return semicolonizeStatement(transformTracked(node, tracked2 || /* @__PURE__ */ new Map()));
32194
+ if (node instanceof RuntimeStatement) {
32195
+ const stmt = node;
32196
+ const lowered = stmt.ast ? lowerJsxInExpression(stmt.ast) : null;
32197
+ if (lowered && lowered !== stmt.ast && (tracked2?.size ?? 0) === 0) {
32198
+ return semicolonizeStatement(printLoweredTs(lowered));
32199
+ }
32200
+ if (lowered && lowered !== stmt.ast) {
32201
+ return semicolonizeStatement(
32202
+ transformTracked({ ...node, ast: lowered }, tracked2 || /* @__PURE__ */ new Map())
32203
+ );
32204
+ }
32205
+ return semicolonizeStatement(transformTracked(node, tracked2 || /* @__PURE__ */ new Map()));
32206
+ }
31918
32207
  if (node instanceof SlotNode) {
31919
32208
  if (__vskMarkerless) return `__out.push(props.children || '');`;
31920
32209
  const sid = `s${nextVskId()}`;
@@ -31931,7 +32220,12 @@ function isEvent(target) {
31931
32220
  return !!target && target.startsWith("on") && target.length > 2;
31932
32221
  }
31933
32222
  function exprJSX(node, tracked2) {
31934
- return exprJS(transformTracked(node, tracked2 || /* @__PURE__ */ new Map()));
32223
+ const lowered = node.ast ? lowerJsxInExpression(node.ast) : null;
32224
+ const didLower = lowered !== node.ast;
32225
+ if (didLower && (tracked2?.size ?? 0) === 0) {
32226
+ return exprJS(printLoweredTs(lowered));
32227
+ }
32228
+ return exprJS(transformTracked({ ...node, ast: lowered }, tracked2 || /* @__PURE__ */ new Map()));
31935
32229
  }
31936
32230
  function staticNodeToJS(node, isAsync = false, tracked2) {
31937
32231
  const lines = [];
@@ -32079,13 +32373,13 @@ function mapRegionToJS(node, isAsync = false, tracked2) {
32079
32373
  }
32080
32374
  function keyedItemTemplate(node) {
32081
32375
  if (!node.keyExpr) return node.bodyTemplate;
32082
- const template = node.bodyTemplate;
32083
- const idx = template.findIndex((n) => n instanceof StaticNode);
32084
- if (idx === -1) return template;
32085
- const root3 = template[idx];
32376
+ const template2 = node.bodyTemplate;
32377
+ const idx = template2.findIndex((n) => n instanceof StaticNode);
32378
+ if (idx === -1) return template2;
32379
+ const root3 = template2[idx];
32086
32380
  const copy = new StaticNode(root3.tag, root3.attributes, [...root3.children, new DynamicBinding(node.keyExpr, "attribute", "data-vsk-key")], root3.keyExpr);
32087
32381
  copy.selfClosing = root3.selfClosing;
32088
- const out = template.slice();
32382
+ const out = template2.slice();
32089
32383
  out[idx] = copy;
32090
32384
  return out;
32091
32385
  }
@@ -32197,18 +32491,24 @@ function componentCallToJS(node, importedNames, isAsync = false, tracked2) {
32197
32491
  }
32198
32492
  }
32199
32493
  if (regularChildren.length > 0) {
32200
- const childLines = [];
32201
- for (const child of regularChildren) {
32202
- const code = irNodeToJS(child, importedNames, isAsync, tracked2);
32203
- if (code) childLines.push(code);
32204
- }
32205
- if (childLines.length > 0) {
32206
- const childrenVar = `__ch${nextVskId()}`;
32207
- propsEntries.push(`children: ${childrenVar}`);
32208
- lines.push(`const ${childrenVar} = ${isAsync ? "await (async " : "("}() => {`);
32209
- lines.push(`const __out = [];`);
32210
- lines.push(indent(childLines.join("\n")));
32211
- lines.push(`return __out.join(''); })();`);
32494
+ const fnType = regularChildren.length === 1 && regularChildren[0] instanceof DynamicBinding && regularChildren[0].kind === "text" ? regularChildren[0].expression.ast?.type : null;
32495
+ const isFnChild = fnType === "ArrowFunctionExpression" || fnType === "FunctionExpression" || fnType === "Identifier" || fnType === "MemberExpression";
32496
+ if (isFnChild) {
32497
+ propsEntries.push(`children: ${exprJSX(regularChildren[0].expression, tracked2)}`);
32498
+ } else {
32499
+ const childLines = [];
32500
+ for (const child of regularChildren) {
32501
+ const code = irNodeToJS(child, importedNames, isAsync, tracked2);
32502
+ if (code) childLines.push(code);
32503
+ }
32504
+ if (childLines.length > 0) {
32505
+ const childrenVar = `__ch${nextVskId()}`;
32506
+ propsEntries.push(`children: ${childrenVar}`);
32507
+ lines.push(`const ${childrenVar} = ${isAsync ? "await (async " : "("}() => {`);
32508
+ lines.push(`const __out = [];`);
32509
+ lines.push(indent(childLines.join("\n")));
32510
+ lines.push(`return __out.join(''); })();`);
32511
+ }
32212
32512
  }
32213
32513
  }
32214
32514
  const propsObj = `{ ${propsEntries.join(", ")} }`;
@@ -32225,9 +32525,9 @@ function componentCallToJS(node, importedNames, isAsync = false, tracked2) {
32225
32525
  }
32226
32526
  const callExpr3 = `${awaitKw}${calleeVar}(${propsObj}, __registry, (${calleeVar}.__veskScope || __vesk))`;
32227
32527
  if (__vskHydrate && !__vskMarkerless) {
32228
- lines.push(`__out.push(${JSON.stringify(componentMarker(compName))} + (${callExpr3} || ''));`);
32528
+ lines.push(`__out.push(${JSON.stringify(componentMarker(compName))} + __flat(${callExpr3}));`);
32229
32529
  } else {
32230
- lines.push(`__out.push(${callExpr3} || '');`);
32530
+ lines.push(`__out.push(__flat(${callExpr3}));`);
32231
32531
  }
32232
32532
  return lines.join("\n");
32233
32533
  }
@@ -32245,6 +32545,7 @@ function generateFunctionBody(comp, importedNames) {
32245
32545
  lines.push(`const __styleText = (v) => { if (typeof v === 'string') return v; if (v && typeof v === 'object') { let s = ''; for (const k in v) { const x = v[k]; if (x == null || x === false) continue; s += k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()) + ':' + x + ';'; } return s; } return String(v); };`);
32246
32546
  lines.push(`const __attr = (n, v) => (v == null || v === false) ? '' : ' ' + n + '="' + __escape(n === 'style' ? __styleText(v) : String(v)) + '"';`);
32247
32547
  lines.push(`const raw = (s) => s == null ? '' : String(s);`);
32548
+ lines.push(`const __flat = (v) => Array.isArray(v) ? v.join('') : (v == null ? '' : String(v));`);
32248
32549
  lines.push(`const __tk = globalThis.__vsk_ssr_token || '';`);
32249
32550
  if (asyncMode) {
32250
32551
  lines.push(`const __pk = __tk ? '__vsk_ssr_promises_' + __tk : '__vsk_ssr_promises';`);
@@ -32390,6 +32691,7 @@ var init_server_jsgen = __esm({
32390
32691
  init_scan();
32391
32692
  init_server_utils();
32392
32693
  init_module_imports();
32694
+ init_server_jsx_lowering();
32393
32695
  init_vsk_imports();
32394
32696
  __currentCompName = "";
32395
32697
  }
@@ -33154,8 +33456,8 @@ function installMdReadHook(publicDirs) {
33154
33456
  while (rel.length > 0 && rel.charCodeAt(0) === 47) rel = rel.slice(1);
33155
33457
  const abs = resolveWithin(dir, rel);
33156
33458
  if (!abs) continue;
33157
- const lower = abs.toLowerCase();
33158
- if (!lower.endsWith(".md") && !lower.endsWith(".markdown")) continue;
33459
+ const lower2 = abs.toLowerCase();
33460
+ if (!lower2.endsWith(".md") && !lower2.endsWith(".markdown")) continue;
33159
33461
  if (existsSync12(abs) && statSync4(abs).isFile()) return readFileSync12(abs, "utf8");
33160
33462
  } catch {
33161
33463
  }
@@ -37582,8 +37884,8 @@ function isPublicMarkdownPath2(v) {
37582
37884
  if (c === 63 || c === 35 || c === 92)
37583
37885
  return false;
37584
37886
  }
37585
- const lower = s.toLowerCase();
37586
- return stringEndsWith2(lower, ".md") || stringEndsWith2(lower, ".markdown");
37887
+ const lower2 = s.toLowerCase();
37888
+ return stringEndsWith2(lower2, ".md") || stringEndsWith2(lower2, ".markdown");
37587
37889
  }
37588
37890
  function getMdPathCell2(path) {
37589
37891
  let cell = mdPathCells2.get(path);
@@ -38323,7 +38625,10 @@ var init_errors2 = __esm({
38323
38625
  suggestions;
38324
38626
  nextSteps;
38325
38627
  tip;
38628
+ /** The V-diagnostic code, e.g. `V0501`. */
38326
38629
  code;
38630
+ /** The rendered code frame. Used to be written into `code`, which silently replaced the V-code. */
38631
+ frame;
38327
38632
  constructor(message, opts = {}) {
38328
38633
  super(message);
38329
38634
  this.name = "VeskError";
@@ -38333,8 +38638,10 @@ var init_errors2 = __esm({
38333
38638
  this.suggestions = opts.suggestions || [];
38334
38639
  this.nextSteps = opts.nextSteps || [];
38335
38640
  this.tip = opts.tip || "";
38336
- if (opts.code !== void 0)
38641
+ if (opts.code !== void 0 && !opts.code.includes("\n"))
38337
38642
  this.code = opts.code;
38643
+ if (opts.frame !== void 0)
38644
+ this.frame = opts.frame;
38338
38645
  }
38339
38646
  static notFound(name, candidates = [], context = {}) {
38340
38647
  const allCandidates = [.../* @__PURE__ */ new Set([...candidates, ...VESK_BUILTINS2])];
@@ -38407,6 +38714,75 @@ var init_errors2 = __esm({
38407
38714
  tip: "Use plain objects for data and factory functions for constructors inside .vsk files."
38408
38715
  });
38409
38716
  }
38717
+ /**
38718
+ * A browser-only global used in code the SERVER evaluates.
38719
+ *
38720
+ * On the server these are either absent (`window is not defined`) or, worse,
38721
+ * silently present-but-wrong (`navigator` exists in modern Node, so
38722
+ * `navigator.onLine` is `undefined` and a page renders as if it were offline).
38723
+ * A raw `ReferenceError` at request time is the worst possible report: it
38724
+ * names neither the component nor the fix. This points at the line.
38725
+ */
38726
+ static ssrUnsafeGlobal(name, context = {}) {
38727
+ return new _VeskError(`\`${name}\` is browser-only, and this component body runs during SSR.`, {
38728
+ ...context,
38729
+ code: context.code || "V0501",
38730
+ suggestions: [
38731
+ `Wrap the part that needs the DOM in a {#client} block.`,
38732
+ `Move the code into an event handler \u2014 handlers never run on the server.`,
38733
+ `Do the setup in on_destroy(), which the compiler only emits on the client.`
38734
+ ],
38735
+ nextSteps: [
38736
+ `component ${context.inComponent ?? "this component"}: wrap the \`${name}\` access in {#client}...{/client}.`,
38737
+ `Or guard it: if (typeof ${name} !== 'undefined') { ... }`
38738
+ ],
38739
+ tip: "SSR renders this component before the client exists. Reading a browser global here either throws during the request or (for `navigator`) silently returns undefined."
38740
+ });
38741
+ }
38742
+ /**
38743
+ * The second name from `track()` called as a function.
38744
+ *
38745
+ * `const &[count, setCount] = track(0)` returns a CELL, not a setter function, so
38746
+ * `setCount(1)` throws `setCount is not a function` from generated code with
38747
+ * no line of user code in the message. The cell is the writer: `.set(v)`.
38748
+ */
38749
+ static cellCalledAsFunction(name, context = {}) {
38750
+ return new _VeskError(`\`${name}\` is a cell, not a function \u2014 cells are written with \`.set()\`.`, {
38751
+ ...context,
38752
+ code: context.code || "V0502",
38753
+ suggestions: [
38754
+ `Use \`${name}.set(...)\` instead of \`${name}(...)\`.`,
38755
+ `Or use the tracked spelling inside an event handler: set(${context.value ?? "value"}, ...)`
38756
+ ],
38757
+ nextSteps: [
38758
+ `const &[${context.value ?? "value"}, ${name}] = track(0) \u2192 \`${name}.set(v)\``,
38759
+ "The value read in markup is the first name; the second is the cell object."
38760
+ ],
38761
+ tip: `\`track()\` returns [value, cell]. \`${name}\` is the cell half.`
38762
+ });
38763
+ }
38764
+ /**
38765
+ * `set(value, …)` used outside a rewritten expression.
38766
+ *
38767
+ * The compiler rewrites `set(count, v)` to `countCell.set(v)` inside JSX and
38768
+ * event handlers — but a bare statement in the component body is NOT
38769
+ * rewritten, so the runtime `set` receives the value and throws
38770
+ * "Cannot read properties of undefined". Pass the CELL, not the value.
38771
+ */
38772
+ static setWithValueName(name, context = {}) {
38773
+ return new _VeskError(`\`set(${name}, \u2026)\` needs the CELL, and \`${name}\` is the value half of track().`, {
38774
+ ...context,
38775
+ code: context.code || "V0503",
38776
+ suggestions: [
38777
+ `Use \`set(${context.cell ?? "cell"}, \u2026)\` or \`${context.cell ?? "cell"}.set(\u2026)\`.`,
38778
+ `Or move the set() into an event handler, where the compiler rewrites it for you.`
38779
+ ],
38780
+ nextSteps: [
38781
+ `const &[${name}, ${context.cell ?? "cell"}] = track(0) \u2192 \`${context.cell ?? "cell"}.set(v)\``
38782
+ ],
38783
+ tip: "set(cell, value) is only rewritten inside JSX and handlers; in a body statement it reaches the runtime untouched."
38784
+ });
38785
+ }
38410
38786
  static serverBlockInClient(compName, context = {}) {
38411
38787
  return new _VeskError(`{#server} block found in client island "${compName}". Client islands render on both server and client, so {#server} blocks have no effect.`, {
38412
38788
  ...context,
@@ -50155,7 +50531,7 @@ import { WebSocketServer as WebSocketServer2 } from "ws";
50155
50531
  // ../compiler/dist/strip-ts.js
50156
50532
  init_parser();
50157
50533
  import { walk as walk6 } from "zimmerframe";
50158
- import { print as print6 } from "esrap";
50534
+ import { print as print7 } from "esrap";
50159
50535
  import ts6 from "esrap/languages/ts";
50160
50536
  var TS_NODE_TYPES2 = /* @__PURE__ */ new Set([
50161
50537
  "TSAsExpression",
@@ -50352,7 +50728,7 @@ function stripCodeTypes2(code) {
50352
50728
  stripped.body = stripped.body.filter((n) => !isTypeOnlyStatement2(n));
50353
50729
  }
50354
50730
  try {
50355
- return print6(stripped, ts6()).code;
50731
+ return print7(stripped, ts6()).code;
50356
50732
  } catch {
50357
50733
  return code;
50358
50734
  }
@@ -50491,7 +50867,7 @@ init_vsk_imports();
50491
50867
  init_md_inline();
50492
50868
  init_strip_ts();
50493
50869
  import { walk as walk7 } from "zimmerframe";
50494
- import { print as print7 } from "esrap";
50870
+ import { print as print8 } from "esrap";
50495
50871
  import ts7 from "esrap/languages/ts";
50496
50872
  import tsx2 from "esrap/languages/tsx";
50497
50873
  function callExpr2(callee, args2 = []) {
@@ -50558,12 +50934,12 @@ function semicolonizeStatement2(code) {
50558
50934
  return code;
50559
50935
  return code + ";";
50560
50936
  }
50561
- function containsJsx2(node, depth = 0) {
50937
+ function containsJsx3(node, depth = 0) {
50562
50938
  if (!node || typeof node !== "object" || depth > 40)
50563
50939
  return false;
50564
50940
  if (Array.isArray(node)) {
50565
50941
  for (const item of node) {
50566
- if (containsJsx2(item, depth + 1))
50942
+ if (containsJsx3(item, depth + 1))
50567
50943
  return true;
50568
50944
  }
50569
50945
  return false;
@@ -50572,13 +50948,13 @@ function containsJsx2(node, depth = 0) {
50572
50948
  return true;
50573
50949
  for (const key of Object.keys(node)) {
50574
50950
  const v = node[key];
50575
- if (v && typeof v === "object" && containsJsx2(v, depth + 1))
50951
+ if (v && typeof v === "object" && containsJsx3(v, depth + 1))
50576
50952
  return true;
50577
50953
  }
50578
50954
  return false;
50579
50955
  }
50580
50956
  function printAst2(ast) {
50581
- return print7(ast, containsJsx2(ast) ? tsx2() : ts7()).code;
50957
+ return print8(ast, containsJsx3(ast) ? tsx2() : ts7()).code;
50582
50958
  }
50583
50959
  function effectBlockToHandlerExpr2(eff) {
50584
50960
  try {
@@ -50699,7 +51075,7 @@ function transformTracked2(irNode, tracked2) {
50699
51075
  return context.next();
50700
51076
  }
50701
51077
  });
50702
- return print7(transformed, containsJsx2(transformed) ? tsx2() : ts7()).code;
51078
+ return print8(transformed, containsJsx3(transformed) ? tsx2() : ts7()).code;
50703
51079
  }
50704
51080
  function collectTrackedNames2(body) {
50705
51081
  const names = /* @__PURE__ */ new Map();
@@ -53834,8 +54210,8 @@ function looksLikeMarkdownPath2(value) {
53834
54210
  return false;
53835
54211
  if (v.includes("\n") || v.includes("\r"))
53836
54212
  return false;
53837
- const lower = v.toLowerCase();
53838
- return lower.endsWith(MD_EXT_MARKER2) || lower.endsWith(".markdown");
54213
+ const lower2 = v.toLowerCase();
54214
+ return lower2.endsWith(MD_EXT_MARKER2) || lower2.endsWith(".markdown");
53839
54215
  }
53840
54216
  function isRelativeSpecifier2(v) {
53841
54217
  return v.startsWith("./") || v.startsWith("../");
@@ -55041,8 +55417,8 @@ function installMdReadHook2(publicDirs) {
55041
55417
  const abs = resolveWithin2(dir, rel);
55042
55418
  if (!abs)
55043
55419
  continue;
55044
- const lower = abs.toLowerCase();
55045
- if (!lower.endsWith(".md") && !lower.endsWith(".markdown"))
55420
+ const lower2 = abs.toLowerCase();
55421
+ if (!lower2.endsWith(".md") && !lower2.endsWith(".markdown"))
55046
55422
  continue;
55047
55423
  if (existsSync29(abs) && statSync15(abs).isFile())
55048
55424
  return readFileSync25(abs, "utf8");
@@ -63160,12 +63536,14 @@ if (cmd === "build") {
63160
63536
  const l = err.line;
63161
63537
  const c = err.column;
63162
63538
  const code = err.code;
63539
+ const frame = err.frame;
63163
63540
  let out = `vesk build: error \u2014 ${err.message}`;
63541
+ if (code) out = `vesk build: [${code}] ${err.message}`;
63164
63542
  if (f) out += `
63165
63543
  File: ${f}${l ? `:${l}${c ? `:${c}` : ""}` : ""}`;
63166
- if (code) out += `
63544
+ if (frame) out += `
63167
63545
 
63168
- ${code}`;
63546
+ ${frame}`;
63169
63547
  console.error(out);
63170
63548
  if (err.stack && !String(err.stack).includes(err.message)) console.error(err.stack);
63171
63549
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/vesk-cli",
3
- "version": "0.2.50-canary.10",
3
+ "version": "0.2.50-canary.12",
4
4
  "type": "module",
5
5
  "description": "Vesk — compiler-first framework for the post-VDOM web. vesk dev, vesk build.",
6
6
  "bin": {
@@ -8,10 +8,10 @@
8
8
  },
9
9
  "main": "./dist/cli.js",
10
10
  "dependencies": {
11
- "@vesk/adapter": "^0.2.50-canary.10",
12
- "@vesk/compiler": "^0.2.50-canary.10",
13
- "@vesk/plugin-tailwind": "^0.2.50-canary.10",
14
- "@vesk/runtime": "^0.2.50-canary.10",
11
+ "@vesk/adapter": "^0.2.50-canary.12",
12
+ "@vesk/compiler": "^0.2.50-canary.12",
13
+ "@vesk/plugin-tailwind": "^0.2.50-canary.12",
14
+ "@vesk/runtime": "^0.2.50-canary.12",
15
15
  "esrap": "^2.3.0",
16
16
  "typescript": "^5.8.0",
17
17
  "ws": "^8.18.0",