@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.
- package/lib/create.js +43 -1
- package/lib/doctor/codes.js +67 -0
- package/lib/doctor/manifest.js +161 -0
- package/lib/doctor/policy.js +124 -0
- package/lib/doctor/probes/elision.js +111 -0
- package/lib/doctor/probes/env.js +53 -0
- package/lib/doctor/probes/framework-resolves.js +260 -0
- package/lib/doctor/probes/git-hook.js +58 -0
- package/lib/doctor/probes/importmap-coherence.js +158 -0
- package/lib/doctor/probes/node.js +37 -0
- package/lib/doctor/probes/static-asset-freshness.js +58 -0
- package/lib/doctor/probes/tsconfig.js +55 -0
- package/lib/doctor/probes/unmarked-asset-links.js +199 -0
- package/lib/doctor/probes/vendor-gitignore.js +84 -0
- package/lib/doctor/probes/vendor-pin.js +77 -0
- package/lib/doctor/probes/webjs-versions.js +85 -0
- package/lib/doctor/route-modules.js +100 -0
- package/lib/doctor/runner.js +72 -0
- package/lib/doctor/util.js +160 -0
- package/lib/doctor.js +5 -1634
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +4 -3
- package/templates/.agents/skills/webjs/SKILL.md +46 -4
- package/templates/.agents/skills/webjs/references/built-ins.md +3 -1
- package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +52 -5
- package/templates/.agents/skills/webjs/references/components.md +50 -2
- package/templates/.agents/skills/webjs/references/module-structure.md +229 -0
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +27 -3
- package/templates/.agents/skills/webjs/references/optimistic-ui.md +23 -11
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +36 -2
- package/templates/.agents/skills/webjs/references/runtime.md +6 -1
- package/templates/.agents/skills/webjs/references/styling.md +47 -2
- package/templates/.github/pull_request_template.md +0 -4
- package/templates/gallery/app/features/boundaries/page.ts +11 -0
- package/templates/gallery/app/features/client-router/page.ts +13 -2
- package/templates/gallery/app/features/metadata/page.ts +7 -1
- package/templates/gallery/modules/client-router/components/router-controls.ts +23 -2
- package/templates/gallery/modules/gallery/nav.ts +35 -26
- package/templates/gallery/modules/stream/components/browser/stream-demo.test.js +64 -0
- package/templates/gallery/modules/stream/components/stream-demo.ts +14 -9
- package/templates/gallery/modules/stream/utils/ui/row.ts +41 -0
- 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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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:
|
|
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
|
});
|