@webjsdev/cli 0.10.55 → 0.10.57

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 (42) hide show
  1. package/lib/create.js +43 -1
  2. package/lib/doctor/codes.js +67 -0
  3. package/lib/doctor/manifest.js +161 -0
  4. package/lib/doctor/policy.js +124 -0
  5. package/lib/doctor/probes/elision.js +111 -0
  6. package/lib/doctor/probes/env.js +53 -0
  7. package/lib/doctor/probes/framework-resolves.js +260 -0
  8. package/lib/doctor/probes/git-hook.js +58 -0
  9. package/lib/doctor/probes/importmap-coherence.js +158 -0
  10. package/lib/doctor/probes/node.js +37 -0
  11. package/lib/doctor/probes/static-asset-freshness.js +58 -0
  12. package/lib/doctor/probes/tsconfig.js +55 -0
  13. package/lib/doctor/probes/unmarked-asset-links.js +199 -0
  14. package/lib/doctor/probes/vendor-gitignore.js +84 -0
  15. package/lib/doctor/probes/vendor-pin.js +77 -0
  16. package/lib/doctor/probes/webjs-versions.js +85 -0
  17. package/lib/doctor/route-modules.js +100 -0
  18. package/lib/doctor/runner.js +72 -0
  19. package/lib/doctor/util.js +160 -0
  20. package/lib/doctor.js +5 -1634
  21. package/package.json +1 -1
  22. package/templates/.agents/rules/workflow.md +4 -3
  23. package/templates/.agents/skills/webjs/SKILL.md +46 -4
  24. package/templates/.agents/skills/webjs/references/built-ins.md +3 -1
  25. package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +52 -5
  26. package/templates/.agents/skills/webjs/references/components.md +50 -2
  27. package/templates/.agents/skills/webjs/references/module-structure.md +229 -0
  28. package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +27 -3
  29. package/templates/.agents/skills/webjs/references/optimistic-ui.md +23 -11
  30. package/templates/.agents/skills/webjs/references/routing-and-pages.md +36 -2
  31. package/templates/.agents/skills/webjs/references/runtime.md +6 -1
  32. package/templates/.agents/skills/webjs/references/styling.md +47 -2
  33. package/templates/.github/pull_request_template.md +0 -4
  34. package/templates/gallery/app/features/boundaries/page.ts +11 -0
  35. package/templates/gallery/app/features/client-router/page.ts +13 -2
  36. package/templates/gallery/app/features/metadata/page.ts +7 -1
  37. package/templates/gallery/modules/client-router/components/router-controls.ts +23 -2
  38. package/templates/gallery/modules/gallery/nav.ts +35 -26
  39. package/templates/gallery/modules/stream/components/browser/stream-demo.test.js +64 -0
  40. package/templates/gallery/modules/stream/components/stream-demo.ts +14 -9
  41. package/templates/gallery/modules/stream/utils/ui/row.ts +41 -0
  42. package/templates/gallery/test/rate-limit/rate-limit.test.ts +2 -1
@@ -0,0 +1,64 @@
1
+ // Co-located browser test for the stream demo, in real Chromium with real SSR
2
+ // and hydration. The runner UI is `tdd` (suite/test) and there is no assertion
3
+ // library, so a tiny inline assert does the job.
4
+ //
5
+ // This pins the one thing the row fragment has to get right: the SEEDED rows
6
+ // (rendered from the html`` shape) and the STREAMED rows (rendered from the
7
+ // string shape) come off one class list, so a mutation cannot leave the list
8
+ // styled two ways. Both shapes are exercised through the real component.
9
+ import { html } from '@webjsdev/core';
10
+ import { ssrFixture } from '@webjsdev/core/testing';
11
+ import '../stream-demo.ts';
12
+
13
+ const assert = (cond, msg) => { if (!cond) throw new Error(msg || 'assertion failed'); };
14
+ const rows = (el) => [...el.querySelectorAll('#stream-list > li')];
15
+ const button = (el, label) => [...el.querySelectorAll('button')].find((b) => b.textContent.trim() === label);
16
+ const tick = () => new Promise((r) => setTimeout(r, 0));
17
+
18
+ suite('<stream-demo>', () => {
19
+ test('SSRs the two seeded rows as direct list children', async () => {
20
+ const el = await ssrFixture(html`<stream-demo></stream-demo>`);
21
+ const ids = rows(el).map((li) => li.id);
22
+ assert(ids.join(',') === 'row-1,row-2', `seeded ids, got ${ids}`);
23
+ // A direct child, no wrapper element between the list and the row. That is
24
+ // the structural reason the row is a fragment and not a display-only element.
25
+ assert(rows(el).every((li) => li.parentElement.id === 'stream-list'), 'rows are direct children of the list');
26
+ });
27
+
28
+ test('a streamed row carries the same classes as a seeded row', async () => {
29
+ const el = await ssrFixture(html`<stream-demo></stream-demo>`);
30
+ const seeded = rows(el)[0].className;
31
+ button(el, 'Append').click();
32
+ await tick();
33
+ const all = rows(el);
34
+ assert(all.length === 3, `three rows after append, got ${all.length}`);
35
+ const streamed = all[2];
36
+ assert(streamed.id === 'row-3', `appended row is row-3, got ${streamed.id}`);
37
+ assert(streamed.className === seeded, `streamed row classes match seeded:\n ${streamed.className}\n ${seeded}`);
38
+ });
39
+
40
+ test('the string shape escapes its holes', async () => {
41
+ // The html`` shape escapes its own holes; this plain-string one has to do
42
+ // it by hand, and it is the shape a reader copies. A raw value here would
43
+ // become markup as soon as renderStream() inserted it.
44
+ const { streamRowHTML } = await import('../../utils/ui/row.ts');
45
+ const out = streamRowHTML('x" onload="boom', '<img src=x onerror=boom>');
46
+ assert(!out.includes('<img'), `text hole is escaped, got ${out}`);
47
+ assert(!out.includes('" onload'), `attribute hole is escaped, got ${out}`);
48
+ });
49
+
50
+ test('replace keeps the id and reset restores the seed list', async () => {
51
+ const el = await ssrFixture(html`<stream-demo></stream-demo>`);
52
+ button(el, 'Replace Row 1').click();
53
+ await tick();
54
+ assert(rows(el)[0].id === 'row-1', 'replace keeps row-1');
55
+ assert(rows(el)[0].textContent.includes('replaced'), 'replace swaps the content');
56
+ button(el, 'Prepend').click();
57
+ await tick();
58
+ button(el, 'Reset').click();
59
+ await tick();
60
+ const ids = rows(el).map((li) => li.id);
61
+ assert(ids.join(',') === 'row-1,row-2', `reset restores the seed list, got ${ids}`);
62
+ assert(rows(el)[0].textContent.trim() === 'Row 1', 'reset restores the seed content');
63
+ });
64
+ });
@@ -13,6 +13,15 @@
13
13
  // region-swap would clobber.
14
14
  import { WebComponent, html, renderStream } from '@webjsdev/core';
15
15
  import { buttonClass } from '#components/ui/button.ts';
16
+ // The row markup lives in ONE place, a feature-local view fragment under
17
+ // `utils/ui/` (see references/styling.md for that folder). A fragment rather
18
+ // than a display-only <stream-row> element because BOTH of this demo's uses
19
+ // rule an element out, which is the test to apply: the seeded list needs a
20
+ // direct `<ul> > <li>` child (a wrapper tag would sit between them and break
21
+ // the selector and the list semantics), and the streamed payload is an HTML
22
+ // STRING, which a component cannot produce at all. Where a wrapper IS fine,
23
+ // prefer the component.
24
+ import { streamRow, streamRowHTML } from '../utils/ui/row.ts';
16
25
 
17
26
  // Build a <webjs-stream> payload string. It is a plain string (NOT an html``
18
27
  // template), so interpolating the row markup here is fine. `remove` needs no
@@ -22,9 +31,6 @@ function streamPayload(action: string, target: string, inner = '') {
22
31
  return `<webjs-stream action="${action}" target="${target}">${body}</webjs-stream>`;
23
32
  }
24
33
 
25
- const rowCls = 'flex items-center gap-2 px-3 py-2 rounded-xl bg-card border border-border text-[15px] text-foreground';
26
- const row = (id: string, label: string) => `<li id="${id}" class="${rowCls}">${label}</li>`;
27
-
28
34
  export class StreamDemo extends WebComponent {
29
35
  // A plain instance field, NOT a signal: incremented to mint unique row ids.
30
36
  // It is never read inside render(), so appending a row does not re-render the
@@ -37,16 +43,16 @@ export class StreamDemo extends WebComponent {
37
43
  // never runs. Name your handlers something else (see muscle-memory-gotchas).
38
44
  appendRow() {
39
45
  this.#n++;
40
- renderStream(streamPayload('append', 'stream-list', row(`row-${this.#n}`, `Row ${this.#n} (appended)`)));
46
+ renderStream(streamPayload('append', 'stream-list', streamRowHTML(`row-${this.#n}`, `Row ${this.#n} (appended)`)));
41
47
  }
42
48
  prependRow() {
43
49
  this.#n++;
44
- renderStream(streamPayload('prepend', 'stream-list', row(`row-${this.#n}`, `Row ${this.#n} (prepended)`)));
50
+ renderStream(streamPayload('prepend', 'stream-list', streamRowHTML(`row-${this.#n}`, `Row ${this.#n} (prepended)`)));
45
51
  }
46
52
  replaceFirst() {
47
53
  // `replace` swaps the target element itself. The replacement keeps id row-1,
48
54
  // so the button stays repeatable.
49
- renderStream(streamPayload('replace', 'row-1', row('row-1', 'Row 1 (replaced)')));
55
+ renderStream(streamPayload('replace', 'row-1', streamRowHTML('row-1', 'Row 1 (replaced)')));
50
56
  }
51
57
  removeSecond() {
52
58
  // `remove` deletes the target and needs no <template>.
@@ -54,7 +60,7 @@ export class StreamDemo extends WebComponent {
54
60
  }
55
61
  reset() {
56
62
  // `update` replaces the target's children, restoring the seed list.
57
- renderStream(streamPayload('update', 'stream-list', row('row-1', 'Row 1') + row('row-2', 'Row 2')));
63
+ renderStream(streamPayload('update', 'stream-list', streamRowHTML('row-1', 'Row 1') + streamRowHTML('row-2', 'Row 2')));
58
64
  }
59
65
 
60
66
  render() {
@@ -71,8 +77,7 @@ export class StreamDemo extends WebComponent {
71
77
  <!-- The target list. renderStream() mutates it by id; this markup renders
72
78
  once and is never re-rendered by the component. -->
73
79
  <ul id="stream-list" class="grid gap-2 m-0 p-0 list-none">
74
- <li id="row-1" class="flex items-center gap-2 px-3 py-2 rounded-xl bg-card border border-border text-[15px] text-foreground">Row 1</li>
75
- <li id="row-2" class="flex items-center gap-2 px-3 py-2 rounded-xl bg-card border border-border text-[15px] text-foreground">Row 2</li>
80
+ ${streamRow('row-1', 'Row 1')}${streamRow('row-2', 'Row 2')}
76
81
  </ul>
77
82
  </div>
78
83
  `;
@@ -0,0 +1,41 @@
1
+ // A feature-local VIEW FRAGMENT: pure, returns markup, used only by the stream
2
+ // demo. It lives in `modules/<feature>/utils/ui/` rather than `utils/` (which
3
+ // holds helpers returning DATA) or `components/` (which holds custom elements).
4
+ // See .agents/skills/webjs/references/styling.md.
5
+ //
6
+ // WHY a fragment and not a display-only <stream-row> element: an element is a
7
+ // tag in the DOM, and neither caller can carry one. The list wants a direct
8
+ // `<ul> > <li>` child, so a wrapper would break that selector and the list
9
+ // semantics; and <webjs-stream> takes its payload as an HTML STRING, which no
10
+ // component can return. That is the test, not bytes: where a wrapper element is
11
+ // harmless, reach for the component instead.
12
+ //
13
+ // Hence the two shapes below, off one class list, so the streamed row and the
14
+ // seeded rows cannot drift apart. That drift was live: the class list used to
15
+ // exist three times in this feature, once in a `rowCls` const and twice inlined
16
+ // in the component's own template, which did not use the const.
17
+ import { html, escapeAttr, escapeText } from '@webjsdev/core';
18
+
19
+ const ROW =
20
+ 'flex items-center gap-2 px-3 py-2 rounded-xl bg-card border border-border text-[15px] text-foreground';
21
+
22
+ /**
23
+ * The row as a template, for the component's own render(). `unknown` on the
24
+ * label because it forwards straight into an `html` hole, which renders a
25
+ * string, a number, a TemplateResult, or an array of those.
26
+ */
27
+ export function streamRow(id: string, label: unknown) {
28
+ return html`<li id=${id} class=${ROW}>${label}</li>`;
29
+ }
30
+
31
+ /**
32
+ * The same row as an HTML string, for a <webjs-stream> template payload.
33
+ *
34
+ * ESCAPE EVERY HOLE HERE. The `html` tag above escapes its own holes; this
35
+ * plain template literal does NOT, so an unescaped value becomes markup the
36
+ * moment renderStream() puts it in the document. The demo only ever passes its
37
+ * own literals, but this is the shape people copy, so it does the safe thing.
38
+ */
39
+ export function streamRowHTML(id: string, label: string): string {
40
+ return `<li id="${escapeAttr(id)}" class="${ROW}">${escapeText(label)}</li>`;
41
+ }
@@ -5,6 +5,7 @@ import { dirname, resolve } from 'node:path';
5
5
 
6
6
  import { createRequestHandler } from '@webjsdev/server';
7
7
  import { testRequest } from '@webjsdev/server/testing';
8
+ import type { Handle } from '@webjsdev/server/testing';
8
9
 
9
10
  const appDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
10
11
 
@@ -22,7 +23,7 @@ const MAX = 5;
22
23
  // X-Forwarded-For that DISAGREES, standing in for the CDN egress address the
23
24
  // real deploy puts there, so a test that passes only because the two agree
24
25
  // cannot exist.
25
- function ping(handle: (req: Request) => Promise<Response>, visitor: string, cdnEgress = '172.68.1.9') {
26
+ function ping(handle: Handle, visitor: string, cdnEgress = '172.68.1.9') {
26
27
  return testRequest(handle, PING, {
27
28
  headers: { 'cf-connecting-ip': visitor, 'x-forwarded-for': cdnEgress },
28
29
  });