leglas 0.8.0 → 0.9.0

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/README.md CHANGED
@@ -12,6 +12,7 @@
12
12
  <a href="https://www.npmjs.com/package/leglas"><img src="https://img.shields.io/npm/v/leglas" alt="npm"></a>
13
13
  <a href="https://github.com/FredAmartey/leglas/actions/workflows/ci.yml"><img src="https://github.com/FredAmartey/leglas/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
14
14
  <a href="LICENSE"><img src="https://img.shields.io/npm/l/leglas" alt="license"></a>
15
+ <a href="https://leglas.vercel.app/changelog/"><img src="https://img.shields.io/badge/changelog-what's%20new-0B1839" alt="changelog"></a>
15
16
  </p>
16
17
 
17
18
  <p align="center">
@@ -534,6 +535,7 @@ pnpm install
534
535
  pnpm build # build every package
535
536
  pnpm test # run the test suite
536
537
  pnpm typecheck # type check every package
538
+ pnpm site # build the site, homepage and changelog, into dist/site
537
539
  ```
538
540
 
539
541
  | Package | Contents |
@@ -549,11 +551,18 @@ terminal and `pnpm --filter @leglas/shell dev` in another.
549
551
  Two packages are published, both unscoped: `leglas`, which bundles the
550
552
  server and the built interface, and `leglas-mcp`. Releases are
551
553
  tag-driven: set the same version in both packages and `plugin.json`, turn
552
- the changelog's Unreleased section into that version, push a `v<version>`
553
- tag, and CI runs the suite and publishes through npm trusted publishing.
554
- A tag that disagrees with the manifests is refused, and so is a patch tag
555
- when `api-surface.txt` has moved since the previous one. No npm token
556
- exists anywhere in the project.
554
+ the changelog's Unreleased section into that version with a title for what
555
+ the release was about, push a `v<version>` tag, and CI runs the suite and
556
+ publishes through npm trusted publishing. A tag that disagrees with the
557
+ manifests is refused, and so is a patch tag when `api-surface.txt` has
558
+ moved since the previous one. No npm token exists anywhere in the project.
559
+
560
+ The [site](https://leglas.vercel.app/) is two pages, the homepage and the
561
+ [changelog](https://leglas.vercel.app/changelog/), written by `site.ts`. The
562
+ changelog page is made from `CHANGELOG.md` and nothing else, so describing a
563
+ release in the changelog is the whole job. Vercel builds it from
564
+ `vercel.json` on every push, so main is the live site and every pull request
565
+ gets a preview.
557
566
 
558
567
  ## License
559
568
 
package/dist/bin.js CHANGED
@@ -2211,6 +2211,32 @@ function createBrowserPool(options = {}) {
2211
2211
  };
2212
2212
  }
2213
2213
 
2214
+ // ../server/dist/hydration.js
2215
+ function hydrationEvidence(messages) {
2216
+ for (const raw of messages) {
2217
+ const message2 = raw.split("\n", 1)[0]?.trim() ?? "";
2218
+ if (/Minified React error #(418|419|422|423|425)\b/.test(message2)) {
2219
+ return { framework: "React", message: message2 };
2220
+ }
2221
+ if (/Hydration failed because/.test(message2) || /error while hydrating/i.test(message2) || /Text content (did not|does not) match/i.test(message2) || /Expected server HTML to contain/i.test(message2) || /did not match\. Server:/.test(message2)) {
2222
+ return { framework: "React", message: message2 };
2223
+ }
2224
+ if (/Hydration (node|text|children|class|style|attribute) mismatch/i.test(message2) || /Hydration completed but contains mismatches/i.test(message2)) {
2225
+ return { framework: "Vue", message: message2 };
2226
+ }
2227
+ if (/hydration_mismatch/.test(message2)) {
2228
+ return { framework: "Svelte", message: message2 };
2229
+ }
2230
+ if (/Hydration Mismatch\. Unable to find DOM nodes/.test(message2)) {
2231
+ return { framework: "Solid", message: message2 };
2232
+ }
2233
+ if (/hydrat/i.test(message2) && (/expected .+ but found/i.test(message2) || /mismatch/i.test(message2) && /(node|element|markup|dom|tag|text|attribute|server|client)/i.test(message2))) {
2234
+ return { framework: "the app", message: message2 };
2235
+ }
2236
+ }
2237
+ return null;
2238
+ }
2239
+
2214
2240
  // ../server/dist/capture.js
2215
2241
  var FRAME_MAX_HEIGHT = 4e3;
2216
2242
  var MIN_WIDTH = 320;
@@ -2304,10 +2330,12 @@ function locatorExpression(focus) {
2304
2330
  async function render(page, input) {
2305
2331
  const width = clamp(Math.round(input.width), MIN_WIDTH, MAX_WIDTH);
2306
2332
  const errors = [];
2333
+ let hydration = null;
2307
2334
  const remember = (value) => {
2335
+ const message2 = String(value ?? "").trim().slice(0, 240);
2336
+ hydration ??= hydrationEvidence([message2]);
2308
2337
  if (errors.length >= 10)
2309
2338
  return;
2310
- const message2 = String(value ?? "").slice(0, 240);
2311
2339
  if (message2 === "" || /favicon/i.test(message2))
2312
2340
  return;
2313
2341
  errors.push(message2);
@@ -2455,7 +2483,7 @@ async function render(page, input) {
2455
2483
  resolved
2456
2484
  });
2457
2485
  }
2458
- return { frame, crops, errors, cut };
2486
+ return { frame, crops, errors, hydration, cut };
2459
2487
  } finally {
2460
2488
  for (const stop of unlisten)
2461
2489
  stop();
@@ -2599,7 +2627,13 @@ async function attachRequest(cwd, requestId, input, deps) {
2599
2627
  const capture = deps.capture ?? capturePage;
2600
2628
  const deadlineMs = deps.deadlineMs ?? 12e3;
2601
2629
  const destination = join5(cwd, CAPTURES_DIR, requestId);
2602
- const captured = { attachments: [], errors: [], cut: false, skipped: null };
2630
+ const captured = {
2631
+ attachments: [],
2632
+ errors: [],
2633
+ hydration: null,
2634
+ cut: false,
2635
+ skipped: null
2636
+ };
2603
2637
  const references = [];
2604
2638
  requestedWidths.set(captured, Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(input.width))));
2605
2639
  const controller = new AbortController();
@@ -2649,6 +2683,7 @@ async function attachRequest(cwd, requestId, input, deps) {
2649
2683
  viewport: direction.frame.width
2650
2684
  });
2651
2685
  captured.errors = direction.errors;
2686
+ captured.hydration = direction.hydration;
2652
2687
  captured.cut = direction.cut;
2653
2688
  for (let index = 0; index < direction.crops.length; index += 1) {
2654
2689
  const crop = direction.crops[index];
@@ -3344,10 +3379,10 @@ function scope(leglasCommand, quotedTitle) {
3344
3379
 
3345
3380
  This is a scoped design change: no test run, no build, and no survey of the rest of the project is needed. The result is checked visually in a live preview, not by tooling.
3346
3381
 
3347
- Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. Keep the change additive: do not rewrite shared components that other directions rely on.`;
3382
+ Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. Keep the change additive: do not rewrite shared components that other directions rely on. A shared script may gain one small per-direction override, at the point it reads what it renders, that defaults to what it renders today; every other direction then renders exactly as before, so that counts as additive.`;
3348
3383
  }
3349
3384
  function capturedBlock(captured) {
3350
- if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.skipped === null)
3385
+ if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.hydration === null && captured.skipped === null)
3351
3386
  return "";
3352
3387
  const lines2 = [];
3353
3388
  const frames = captured.attachments.filter((attachment) => attachment.kind === "frame" || attachment.kind === "note");
@@ -3379,6 +3414,9 @@ function capturedBlock(captured) {
3379
3414
  for (const error of captured.errors)
3380
3415
  lines2.push(` - ${error}`);
3381
3416
  }
3417
+ if (captured.hydration !== null) {
3418
+ lines2.push(`After load, ${captured.hydration.framework} rebuilt this page in the browser from the app's own JavaScript and data (${captured.hydration.message}). Markup edited in the served HTML shows for a moment and is then replaced, so make the change where that JavaScript gets what it renders: the data or source it reads, or a per-direction override that a shared script reads with the original as its default. Look at the result a few seconds after load, not at first paint.`);
3419
+ }
3382
3420
  if (captured.skipped !== null) {
3383
3421
  lines2.push(`(${captured.skipped} Use the live preview instead.)`);
3384
3422
  }
@@ -6060,6 +6098,7 @@ async function startServer(options) {
6060
6098
  height: shot.height,
6061
6099
  viewport: result2.frame.width,
6062
6100
  errors: result2.errors,
6101
+ hydration: result2.hydration,
6063
6102
  cut: result2.cut
6064
6103
  });
6065
6104
  } catch (error) {
@@ -6803,6 +6842,14 @@ When asked for design variations, alternatives, or "a few options":
6803
6842
  and register it with \`npx leglas add --title "\u2026" --url "/" --branch <branch>\`
6804
6843
  (the config needs \`devCommand\` with \`{port}\`). Everything below is the
6805
6844
  ordinary, in-app path.
6845
+ A page the app rebuilds in the browser after load (anything that hydrates:
6846
+ Next, Nuxt, SvelteKit, a captured production site) is not its served HTML.
6847
+ Markup edited there shows for a moment and is then replaced from the app's
6848
+ own JavaScript and data, so make the change where that JavaScript gets what
6849
+ it renders. When that is a script other directions share, give it a
6850
+ per-direction override that defaults to what it renders today: every other
6851
+ direction renders exactly as before, which is adding beside, not rewriting.
6852
+ \`npx leglas show\` says when a page was rebuilt after load.
6806
6853
  2. Run \`npx leglas explore <surface> --count <n>\` first, adding
6807
6854
  \`--based-on "<title>"\` when the user wants variations of a direction they
6808
6855
  already like. It prints what the set needs and how to register it. In
@@ -7566,6 +7613,10 @@ async function runShow(options, deps) {
7566
7613
  height: captured.height,
7567
7614
  viewport: captured.viewport,
7568
7615
  errors: Array.isArray(captured.errors) ? captured.errors.filter((error) => typeof error === "string") : [],
7616
+ hydration: typeof captured.hydration === "object" && captured.hydration !== null && typeof captured.hydration.framework === "string" && typeof captured.hydration.message === "string" ? {
7617
+ framework: captured.hydration.framework,
7618
+ message: captured.hydration.message
7619
+ } : null,
7569
7620
  cut: captured.cut === true
7570
7621
  };
7571
7622
  }
@@ -7593,6 +7644,12 @@ async function runShow(options, deps) {
7593
7644
  if (envelope2.screenshot.cut) {
7594
7645
  deps.log(" the top of the page only; it is taller than one capture");
7595
7646
  }
7647
+ if (envelope2.screenshot.hydration !== null) {
7648
+ deps.log(
7649
+ ` hydration ${envelope2.screenshot.hydration.framework} rebuilt the page in the browser after load; the served markup is not what is on screen`
7650
+ );
7651
+ deps.log(` ${envelope2.screenshot.hydration.message}`);
7652
+ }
7596
7653
  if (envelope2.screenshot.errors.length > 0) {
7597
7654
  const count = envelope2.screenshot.errors.length;
7598
7655
  deps.log(` console ${count} ${count === 1 ? "error" : "errors"} on load`);
package/dist/index.js CHANGED
@@ -688,6 +688,14 @@ When asked for design variations, alternatives, or "a few options":
688
688
  and register it with \`npx leglas add --title "\u2026" --url "/" --branch <branch>\`
689
689
  (the config needs \`devCommand\` with \`{port}\`). Everything below is the
690
690
  ordinary, in-app path.
691
+ A page the app rebuilds in the browser after load (anything that hydrates:
692
+ Next, Nuxt, SvelteKit, a captured production site) is not its served HTML.
693
+ Markup edited there shows for a moment and is then replaced from the app's
694
+ own JavaScript and data, so make the change where that JavaScript gets what
695
+ it renders. When that is a script other directions share, give it a
696
+ per-direction override that defaults to what it renders today: every other
697
+ direction renders exactly as before, which is adding beside, not rewriting.
698
+ \`npx leglas show\` says when a page was rebuilt after load.
691
699
  2. Run \`npx leglas explore <surface> --count <n>\` first, adding
692
700
  \`--based-on "<title>"\` when the user wants variations of a direction they
693
701
  already like. It prints what the set needs and how to register it. In
@@ -2593,6 +2601,32 @@ function createBrowserPool(options = {}) {
2593
2601
  };
2594
2602
  }
2595
2603
 
2604
+ // ../server/dist/hydration.js
2605
+ function hydrationEvidence(messages) {
2606
+ for (const raw of messages) {
2607
+ const message2 = raw.split("\n", 1)[0]?.trim() ?? "";
2608
+ if (/Minified React error #(418|419|422|423|425)\b/.test(message2)) {
2609
+ return { framework: "React", message: message2 };
2610
+ }
2611
+ if (/Hydration failed because/.test(message2) || /error while hydrating/i.test(message2) || /Text content (did not|does not) match/i.test(message2) || /Expected server HTML to contain/i.test(message2) || /did not match\. Server:/.test(message2)) {
2612
+ return { framework: "React", message: message2 };
2613
+ }
2614
+ if (/Hydration (node|text|children|class|style|attribute) mismatch/i.test(message2) || /Hydration completed but contains mismatches/i.test(message2)) {
2615
+ return { framework: "Vue", message: message2 };
2616
+ }
2617
+ if (/hydration_mismatch/.test(message2)) {
2618
+ return { framework: "Svelte", message: message2 };
2619
+ }
2620
+ if (/Hydration Mismatch\. Unable to find DOM nodes/.test(message2)) {
2621
+ return { framework: "Solid", message: message2 };
2622
+ }
2623
+ if (/hydrat/i.test(message2) && (/expected .+ but found/i.test(message2) || /mismatch/i.test(message2) && /(node|element|markup|dom|tag|text|attribute|server|client)/i.test(message2))) {
2624
+ return { framework: "the app", message: message2 };
2625
+ }
2626
+ }
2627
+ return null;
2628
+ }
2629
+
2596
2630
  // ../server/dist/capture.js
2597
2631
  var FRAME_MAX_HEIGHT = 4e3;
2598
2632
  var MIN_WIDTH = 320;
@@ -2686,10 +2720,12 @@ function locatorExpression(focus) {
2686
2720
  async function render(page, input) {
2687
2721
  const width = clamp(Math.round(input.width), MIN_WIDTH, MAX_WIDTH);
2688
2722
  const errors = [];
2723
+ let hydration = null;
2689
2724
  const remember = (value) => {
2725
+ const message2 = String(value ?? "").trim().slice(0, 240);
2726
+ hydration ??= hydrationEvidence([message2]);
2690
2727
  if (errors.length >= 10)
2691
2728
  return;
2692
- const message2 = String(value ?? "").slice(0, 240);
2693
2729
  if (message2 === "" || /favicon/i.test(message2))
2694
2730
  return;
2695
2731
  errors.push(message2);
@@ -2837,7 +2873,7 @@ async function render(page, input) {
2837
2873
  resolved
2838
2874
  });
2839
2875
  }
2840
- return { frame, crops, errors, cut };
2876
+ return { frame, crops, errors, hydration, cut };
2841
2877
  } finally {
2842
2878
  for (const stop of unlisten)
2843
2879
  stop();
@@ -2981,7 +3017,13 @@ async function attachRequest(cwd, requestId, input, deps) {
2981
3017
  const capture = deps.capture ?? capturePage;
2982
3018
  const deadlineMs = deps.deadlineMs ?? 12e3;
2983
3019
  const destination = join5(cwd, CAPTURES_DIR, requestId);
2984
- const captured = { attachments: [], errors: [], cut: false, skipped: null };
3020
+ const captured = {
3021
+ attachments: [],
3022
+ errors: [],
3023
+ hydration: null,
3024
+ cut: false,
3025
+ skipped: null
3026
+ };
2985
3027
  const references = [];
2986
3028
  requestedWidths.set(captured, Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(input.width))));
2987
3029
  const controller = new AbortController();
@@ -3031,6 +3073,7 @@ async function attachRequest(cwd, requestId, input, deps) {
3031
3073
  viewport: direction.frame.width
3032
3074
  });
3033
3075
  captured.errors = direction.errors;
3076
+ captured.hydration = direction.hydration;
3034
3077
  captured.cut = direction.cut;
3035
3078
  for (let index = 0; index < direction.crops.length; index += 1) {
3036
3079
  const crop = direction.crops[index];
@@ -3726,10 +3769,10 @@ function scope(leglasCommand, quotedTitle) {
3726
3769
 
3727
3770
  This is a scoped design change: no test run, no build, and no survey of the rest of the project is needed. The result is checked visually in a live preview, not by tooling.
3728
3771
 
3729
- Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. Keep the change additive: do not rewrite shared components that other directions rely on.`;
3772
+ Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. Keep the change additive: do not rewrite shared components that other directions rely on. A shared script may gain one small per-direction override, at the point it reads what it renders, that defaults to what it renders today; every other direction then renders exactly as before, so that counts as additive.`;
3730
3773
  }
3731
3774
  function capturedBlock(captured) {
3732
- if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.skipped === null)
3775
+ if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.hydration === null && captured.skipped === null)
3733
3776
  return "";
3734
3777
  const lines2 = [];
3735
3778
  const frames = captured.attachments.filter((attachment) => attachment.kind === "frame" || attachment.kind === "note");
@@ -3761,6 +3804,9 @@ function capturedBlock(captured) {
3761
3804
  for (const error of captured.errors)
3762
3805
  lines2.push(` - ${error}`);
3763
3806
  }
3807
+ if (captured.hydration !== null) {
3808
+ lines2.push(`After load, ${captured.hydration.framework} rebuilt this page in the browser from the app's own JavaScript and data (${captured.hydration.message}). Markup edited in the served HTML shows for a moment and is then replaced, so make the change where that JavaScript gets what it renders: the data or source it reads, or a per-direction override that a shared script reads with the original as its default. Look at the result a few seconds after load, not at first paint.`);
3809
+ }
3764
3810
  if (captured.skipped !== null) {
3765
3811
  lines2.push(`(${captured.skipped} Use the live preview instead.)`);
3766
3812
  }
@@ -6442,6 +6488,7 @@ async function startServer(options) {
6442
6488
  height: shot.height,
6443
6489
  viewport: result.frame.width,
6444
6490
  errors: result.errors,
6491
+ hydration: result.hydration,
6445
6492
  cut: result.cut
6446
6493
  });
6447
6494
  } catch (error) {
@@ -7474,6 +7521,10 @@ async function runShow(options, deps) {
7474
7521
  height: captured.height,
7475
7522
  viewport: captured.viewport,
7476
7523
  errors: Array.isArray(captured.errors) ? captured.errors.filter((error) => typeof error === "string") : [],
7524
+ hydration: typeof captured.hydration === "object" && captured.hydration !== null && typeof captured.hydration.framework === "string" && typeof captured.hydration.message === "string" ? {
7525
+ framework: captured.hydration.framework,
7526
+ message: captured.hydration.message
7527
+ } : null,
7477
7528
  cut: captured.cut === true
7478
7529
  };
7479
7530
  }
@@ -7501,6 +7552,12 @@ async function runShow(options, deps) {
7501
7552
  if (envelope2.screenshot.cut) {
7502
7553
  deps.log(" the top of the page only; it is taller than one capture");
7503
7554
  }
7555
+ if (envelope2.screenshot.hydration !== null) {
7556
+ deps.log(
7557
+ ` hydration ${envelope2.screenshot.hydration.framework} rebuilt the page in the browser after load; the served markup is not what is on screen`
7558
+ );
7559
+ deps.log(` ${envelope2.screenshot.hydration.message}`);
7560
+ }
7504
7561
  if (envelope2.screenshot.errors.length > 0) {
7505
7562
  const count = envelope2.screenshot.errors.length;
7506
7563
  deps.log(` console ${count} ${count === 1 ? "error" : "errors"} on load`);