@torpor/view 1.0.3 → 1.1.1

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/compile.mjs CHANGED
@@ -2200,12 +2200,15 @@ function headElementSelector(node) {
2200
2200
  function buildElementAttributes$1(node, varName, status, b) {
2201
2201
  let fragment = status.fragmentStack.at(-1)?.fragment;
2202
2202
  if (!fragment) throw new Error("No fragment on stack");
2203
- for (let { name, value, reactive, fullyReactive, span } of node.attributes) if (name === "self" && node.tagName === "@element") {} else if (name === "&ref") {} else if (value != null && fullyReactive) {
2204
- if (name === "&group") buildBindGroupAttribute(node, varName, value, status, b);
2205
- else if (name === "&value" || name === "&checked") buildBindAttribute(node, varName, name, value, status, b);
2203
+ const combinedHandlers = combinedEventHandlers(node, status);
2204
+ const consumedHandlers = new Set(Array.from(combinedHandlers.values(), (c) => c.index));
2205
+ for (let [index, { name, value, reactive, fullyReactive, span }] of node.attributes.entries()) if (name === "self" && node.tagName === "@element") {} else if (name === "&ref") {} else if (value != null && fullyReactive) {
2206
+ if (name === "&group") buildBindGroupAttribute(node, varName, value, status, b, combinedHandlers.get(index));
2207
+ else if (name === "&value" || name === "&checked") buildBindAttribute(node, varName, name, value, status, b, combinedHandlers.get(index));
2206
2208
  else if (name === "onmount") buildMount("elMount", `return (${trimEnd(value.trim(), ";")})(${varName});`, status, b);
2207
- else if (name.startsWith("on")) buildEventAttribute(varName, name, value, span, status, b);
2208
- else if (name.startsWith("transition")) buildTransitionAttribute(node, varName, name, value, status, b);
2209
+ else if (name.startsWith("on")) {
2210
+ if (!consumedHandlers.has(index)) buildEventAttribute(varName, name, value, span, status, b);
2211
+ } else if (name.startsWith("transition")) buildTransitionAttribute(node, varName, name, value, status, b);
2209
2212
  else if (name === "class") {
2210
2213
  status.imports.add("t_class");
2211
2214
  const params = [value];
@@ -2243,21 +2246,20 @@ function buildElementAttributes$1(node, varName, status, b) {
2243
2246
  }
2244
2247
  }
2245
2248
  }
2246
- function buildBindGroupAttribute(node, varName, value, status, b) {
2249
+ function buildBindGroupAttribute(node, varName, value, status, b, combined) {
2247
2250
  value = replaceForVarNames(value, status);
2248
2251
  let eventName = "change";
2249
2252
  let inputValue = node.attributes.find((a) => a.name === "value")?.value;
2250
2253
  let set = `${value} == ${inputValue}`;
2251
2254
  let propName = "checked";
2252
2255
  buildRun("setBinding", `${`${varName}.${propName} = ${set}`};`, status, b);
2253
- status.imports.add("t_event");
2254
- b.append(`t_event(${varName}, "${eventName}", (e) => {
2255
- if (e.target.${propName}) ${value} = ${inputValue};
2256
- });`);
2256
+ emitBindEvent(varName, eventName, `(e) => {
2257
+ if (e.target.${propName}) ${value} = ${inputValue};
2258
+ }`, `if (e.target.${propName}) ${value} = ${inputValue};`, combined, status, b);
2257
2259
  }
2258
- function buildBindAttribute(node, varName, name, value, status, b) {
2260
+ function buildBindAttribute(node, varName, name, value, status, b, combined) {
2259
2261
  value = replaceForVarNames(value, status);
2260
- let eventName = "input";
2262
+ let eventName = bindEventName(node, name);
2261
2263
  let defaultValue = "\"\"";
2262
2264
  let inputValue = "e.target.value";
2263
2265
  if (node.tagName === "input") {
@@ -2270,25 +2272,83 @@ function buildBindAttribute(node, varName, name, value, status, b) {
2270
2272
  case "checkbox":
2271
2273
  defaultValue = "false";
2272
2274
  inputValue = "e.target.checked";
2273
- break;
2274
- case "radio":
2275
- eventName = "change";
2276
- inputValue = "e.target.value";
2277
2275
  }
2278
2276
  } else if (node.tagName === "select") {
2279
- eventName = "change";
2280
2277
  if (node.attributes.find((a) => a.name === "multiple")) {
2281
2278
  buildRun("setBinding", `Array.from(${varName}.options).forEach((opt) => opt.selected = Array.isArray(${value}) && ${value}.includes(opt.value));`, status, b);
2282
- status.imports.add("t_event");
2283
- b.append(`t_event(${varName}, "change", (e) => ${value} = Array.from(e.target.selectedOptions).map((opt) => opt.value));`);
2279
+ emitBindEvent(varName, eventName, `(e) => ${value} = Array.from(e.target.selectedOptions).map((opt) => opt.value)`, `${value} = Array.from(e.target.selectedOptions).map((opt) => opt.value);`, combined, status, b);
2284
2280
  return;
2285
2281
  }
2286
2282
  }
2287
2283
  let set = `${value} || ${defaultValue}`;
2288
2284
  const propName = name.substring(1);
2289
2285
  buildRun("setBinding", `${propName === "value" ? `${varName}.${propName} = String(${set})` : `${varName}.${propName} = ${set}`};`, status, b);
2286
+ emitBindEvent(varName, eventName, `(e) => ${value} = ${inputValue}`, `${value} = ${inputValue};`, combined, status, b);
2287
+ }
2288
+ /**
2289
+ * Gets the name of the event that a &-binding attribute uses to write the
2290
+ * element's state back to the bound value
2291
+ */
2292
+ function bindEventName(node, name) {
2293
+ if (name === "&group") return "change";
2294
+ if (node.tagName === "select") return "change";
2295
+ if (node.tagName === "input") {
2296
+ let typeAttribute = node.attributes.find((a) => a.name === "type");
2297
+ if (typeAttribute?.value && trimQuotes(typeAttribute.value) === "radio") return "change";
2298
+ }
2299
+ return "input";
2300
+ }
2301
+ /**
2302
+ * Pairs &-binding attributes (&value, &checked, &group) with user `on<event>`
2303
+ * handler attributes listening to the same event. Both would emit their own
2304
+ * t_event call for the element, but the second registration would clobber
2305
+ * the first -- typically the binding's state write -- so the pair is emitted
2306
+ * as one t_event that calls the binding write and the user handler in source
2307
+ * order. Returns a map from binding attribute index to the combined handler.
2308
+ */
2309
+ function combinedEventHandlers(node, status) {
2310
+ const bindings = /* @__PURE__ */ new Map();
2311
+ const handlers = /* @__PURE__ */ new Map();
2312
+ for (let [index, { name, value, fullyReactive }] of node.attributes.entries()) {
2313
+ if (value == null || !fullyReactive) continue;
2314
+ if (name === "&value" || name === "&checked" || name === "&group") bindings.set(bindEventName(node, name), index);
2315
+ else if (name.startsWith("on")) handlers.set(name.substring(2), index);
2316
+ }
2317
+ const combined = /* @__PURE__ */ new Map();
2318
+ for (let [eventName, bindingIndex] of bindings) {
2319
+ const handlerIndex = handlers.get(eventName);
2320
+ if (handlerIndex === void 0) continue;
2321
+ const handler = node.attributes[handlerIndex];
2322
+ combined.set(bindingIndex, {
2323
+ index: handlerIndex,
2324
+ value: replaceForVarNames(handler.value, status),
2325
+ span: handler.span,
2326
+ first: handlerIndex < bindingIndex
2327
+ });
2328
+ }
2329
+ return combined;
2330
+ }
2331
+ /**
2332
+ * Emits the t_event call for a &-binding, optionally combined with a user
2333
+ * `on<event>` handler for the same event (which is called after -- or, when
2334
+ * it appeared first in source, before -- the binding write)
2335
+ */
2336
+ function emitBindEvent(varName, eventName, listener, body, combined, status, b) {
2290
2337
  status.imports.add("t_event");
2291
- b.append(`t_event(${varName}, "${eventName}", (e) => ${value} = ${inputValue});`);
2338
+ if (!combined) {
2339
+ b.append(`t_event(${varName}, "${eventName}", ${listener});`);
2340
+ return;
2341
+ }
2342
+ b.append(`t_event(${varName}, "${eventName}", (e) => {`);
2343
+ const handlerCall = `((${combined.value}) as ((e: any) => any) | undefined)?.(e);`;
2344
+ if (combined.first) {
2345
+ addMappedText("", handlerCall, "", combined.span, status, b);
2346
+ b.append(body);
2347
+ } else {
2348
+ b.append(body);
2349
+ addMappedText("", handlerCall, "", combined.span, status, b);
2350
+ }
2351
+ b.append("});");
2292
2352
  }
2293
2353
  function buildEventAttribute(varName, name, value, span, status, b) {
2294
2354
  value = replaceForVarNames(value, status);
@@ -2799,7 +2859,7 @@ function buildServerComponentNode(node, status, b) {
2799
2859
  const nameAttribute = slot.attributes.find((a) => a.name === "name");
2800
2860
  const slotName = nameAttribute?.value ? trimQuotes(nameAttribute.value) : "_";
2801
2861
  const slotParams = [slot.hasSlotProps ? "$slot: Record<PropertyKey, any>" : "_$slot?: Record<PropertyKey, any>", "// @ts-ignore\n// eslint-disable-next-line no-unused-vars\n$context?: Record<PropertyKey, any>"];
2802
- b.append(`${slot.hasSlotProps ? "// @ts-ignore\n" : ""}${slotsName}["${slotName}"] = (\n${slotParams.join(",\n")}\n) => {`);
2862
+ b.append(`${slot.hasSlotProps ? "// @ts-ignore\n" : ""}${slotsName}["${slotName}"] = async (\n${slotParams.join(",\n")}\n) => {`);
2803
2863
  b.append(`let t_body = "";`);
2804
2864
  for (let child of slot.children) buildServerNode(child, status, b);
2805
2865
  if (status.output) {
@@ -2818,7 +2878,7 @@ function buildServerComponentNode(node, status, b) {
2818
2878
  let renderParams = `${propsName}, $context`;
2819
2879
  if (slotsName !== "undefined") renderParams += `, ${slotsName}`;
2820
2880
  const componentResult = nextVarName("comp", status);
2821
- b.append(`const ${componentResult} = ${componentName}(${renderParams});`);
2881
+ b.append(`const ${componentResult} = await ${componentName}(${renderParams});`);
2822
2882
  b.append(`${status.inHead ? "t_head" : "t_body"} += ${componentResult}.body;`);
2823
2883
  b.append(`t_head += ${componentResult}.head;`);
2824
2884
  status.output += HYDRATION_END_COMMENT;
@@ -2826,16 +2886,37 @@ function buildServerComponentNode(node, status, b) {
2826
2886
  }
2827
2887
  //#endregion
2828
2888
  //#region src/compile/build/server/buildServerAwaitNode.ts
2889
+ /**
2890
+ * Builds an `@await` boundary for the server (ASYNC.md → "Server rendering").
2891
+ *
2892
+ * Rather than inlining the `with` branch (the old shape), the boundary is
2893
+ * handed to the `t_await_server` runtime helper: content and `with` branches
2894
+ * become closures, and the helper decides at render time whether resolved
2895
+ * content can ship (all `source: "server"` reads settled) or the `with`
2896
+ * branch + client fetch is the honest output. The runtime emits the
2897
+ * hydration markers and anchor itself, including the `<!--t-await:...-->`
2898
+ * values payload for resolved boundaries.
2899
+ */
2829
2900
  function buildServerAwaitNode(node, status, b) {
2830
- status.output += HYDRATION_START_COMMENT;
2831
2901
  flushOutput(status, b);
2832
- for (let branch of node.children) if (isControlNode(branch) && branch.operation === "@with") buildServerAwaitBranch(branch, status, b);
2833
- status.output += HYDRATION_END_COMMENT;
2834
- status.output += "<!>";
2835
- }
2836
- function buildServerAwaitBranch(node, status, b) {
2837
- for (let child of node.children) buildServerNode(child, status, b);
2902
+ status.imports.add("t_await_server");
2903
+ const id = status.awaitCount++;
2904
+ const resultName = nextVarName("await", status);
2905
+ b.append(`const ${resultName} = await t_await_server("${id}", async () => {`);
2906
+ b.append(`let t_body = "";`);
2907
+ b.append(`let t_head = "";`);
2908
+ for (let branch of node.children) if (isControlNode(branch) && branch.operation === "@await") for (let child of branch.children) buildServerNode(child, status, b);
2909
+ flushOutput(status, b);
2910
+ b.append(`return { body: t_body, head: t_head };`);
2911
+ b.append(`}, async () => {`);
2912
+ b.append(`let t_body = "";`);
2913
+ b.append(`let t_head = "";`);
2914
+ for (let branch of node.children) if (isControlNode(branch) && branch.operation === "@with") for (let child of branch.children) buildServerNode(child, status, b);
2838
2915
  flushOutput(status, b);
2916
+ b.append(`return { body: t_body, head: t_head };`);
2917
+ b.append(`});`);
2918
+ b.append(`${status.inHead ? "t_head" : "t_body"} += ${resultName}.body;`);
2919
+ b.append(`t_head += ${resultName}.head;`);
2839
2920
  }
2840
2921
  //#endregion
2841
2922
  //#region src/compile/build/server/buildServerForNode.ts
@@ -3090,7 +3171,7 @@ function buildServerSlotNode(node, status, b) {
3090
3171
  else b.append(`${propsName}["${name}"] = true;`);
3091
3172
  }
3092
3173
  b.append(`if ($slots && $slots["${slotName}"]) {`);
3093
- b.append(`${status.inHead ? "t_head" : "t_body"} += $slots["${slotName}"](${slotHasProps ? propsName : "undefined"}, $context);`);
3174
+ b.append(`${status.inHead ? "t_head" : "t_body"} += await $slots["${slotName}"](${slotHasProps ? propsName : "undefined"}, $context);`);
3094
3175
  const fill = node.children.find((c) => isSpecialNode(c) && (c.tagName === "fill" || c.tagName === "filldef"));
3095
3176
  if (fill && isSpecialNode(fill) && fill.children.length) {
3096
3177
  b.append(`} else {`);
@@ -3193,6 +3274,8 @@ const importsMap = {
3193
3274
  $unwrap: "import { $unwrap } from \"${folder}\";",
3194
3275
  $peek: "import { $peek } from \"${folder}\";",
3195
3276
  $batch: "import { $batch } from \"${folder}\";",
3277
+ t_server_flush: "import { t_server_flush } from \"${folder}\";",
3278
+ t_await_server: "import { t_await_server } from \"${folder}\";",
3196
3279
  t_fmt: "import { t_fmt } from \"${folder}\";",
3197
3280
  t_attr: "import { t_attr } from \"${folder}\";",
3198
3281
  t_class: "import { t_class } from \"${folder}\";",
@@ -3235,18 +3318,24 @@ function buildServerTemplate(template, imports, b, options) {
3235
3318
  if (/\$unwrap\b/.test(scriptCode)) imports.add("$unwrap");
3236
3319
  if (/\$peek\b/.test(scriptCode)) imports.add("$peek");
3237
3320
  if (/\$batch\b/.test(scriptCode)) imports.add("$batch");
3321
+ const asyncChunks = /* @__PURE__ */ new Set();
3322
+ for (let [i, chunk] of template.script.entries()) if (chunk.script === "/* @params */" && i > 0) asyncChunks.add(i - 1);
3238
3323
  let currentIndex = 0;
3239
3324
  let current = template.components[0];
3240
- for (let chunk of template.script) if (chunk.script === "/* @params */") {
3325
+ for (let [chunkIndex, chunk] of template.script.entries()) if (chunk.script === "/* @params */") {
3241
3326
  let params = [
3242
3327
  current.params ?? `${current.props?.length ? "$props: Record<PropertyKey, any>" : "_$props?: Record<PropertyKey, any>"}`,
3243
3328
  `${current.contextProps?.length || current.markup && markupRendersComponent(current.markup) || current.error && markupRendersComponent(current.error) ? "$context" : "_$context"}?: Record<PropertyKey, any>`,
3244
3329
  `${current.slotProps?.length ? "$slots" : "_$slots"}?: Record<string, ServerSlotRender>`
3245
3330
  ];
3246
3331
  b.append(params.join(",\n") + ",");
3247
- } else if (chunk.script === ") /* @return_type */ {") b.append("): { body: string; head: string } {");
3332
+ } else if (chunk.script === ") /* @return_type */ {") b.append("): Promise<{ body: string; head: string }> {");
3248
3333
  else if (chunk.script === "/* @start */") {
3249
3334
  if (current.contextProps?.length) b.append(`$context = Object.assign({}, $context);`);
3335
+ if (containsAwaitGroup(current.markup) || containsAwaitGroup(current.error) || containsAwaitGroup(current.head)) {
3336
+ imports.add("t_server_flush");
3337
+ b.append(`return t_server_flush(async () => {`);
3338
+ }
3250
3339
  b.append(`let t_body = "";`);
3251
3340
  b.append(`let t_head = "";`);
3252
3341
  } else if (chunk.script === "/* @render */") {
@@ -3257,6 +3346,7 @@ function buildServerTemplate(template, imports, b, options) {
3257
3346
  styleHash: current.style?.hash || "",
3258
3347
  varNames: {},
3259
3348
  preserveWhitespace: false,
3349
+ awaitCount: 0,
3260
3350
  options
3261
3351
  };
3262
3352
  b.append("");
@@ -3280,6 +3370,7 @@ function buildServerTemplate(template, imports, b, options) {
3280
3370
  styleHash: current.style?.hash || "",
3281
3371
  varNames: {},
3282
3372
  preserveWhitespace: false,
3373
+ awaitCount: 0,
3283
3374
  options
3284
3375
  };
3285
3376
  b.append(`} catch (${current.errorVar}) {`);
@@ -3302,6 +3393,7 @@ function buildServerTemplate(template, imports, b, options) {
3302
3393
  varNames: {},
3303
3394
  preserveWhitespace: false,
3304
3395
  inHead: true,
3396
+ awaitCount: 0,
3305
3397
  options
3306
3398
  };
3307
3399
  b.append("");
@@ -3318,9 +3410,23 @@ function buildServerTemplate(template, imports, b, options) {
3318
3410
  }
3319
3411
  } else if (chunk.script === "/* @end */") {
3320
3412
  b.append(`return { body: t_body, head: t_head };`);
3413
+ if (containsAwaitGroup(current.markup) || containsAwaitGroup(current.error) || containsAwaitGroup(current.head)) b.append(`});`);
3321
3414
  currentIndex += 1;
3322
3415
  current = template.components[currentIndex];
3323
- } else b.append(chunk.script);
3416
+ } else if (asyncChunks.has(chunkIndex)) b.append(chunk.script.replace(/(?<!async\s)function\s+([A-Za-z_$][\w$]*)((?:<(?:[^<>]|<[^<>]*>)*>)?)\s*\($/, "async function $1$2("));
3417
+ else b.append(chunk.script);
3418
+ }
3419
+ /**
3420
+ * Whether a markup tree contains an `@await` boundary, so the component's
3421
+ * render must be wrapped in a flush.
3422
+ */
3423
+ function containsAwaitGroup(node) {
3424
+ if (node === void 0 || node === null) return false;
3425
+ if (isControlNode(node) && node.operation === "@await group") return true;
3426
+ if (node.type !== "text" && node.type !== "comment") {
3427
+ for (let child of node.children) if (containsAwaitGroup(child)) return true;
3428
+ }
3429
+ return false;
3324
3430
  }
3325
3431
  //#endregion
3326
3432
  //#region src/compile/utils/trimWhitespace.ts