You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(core): Class.register() — drop the import.meta.url argument
Authors no longer write `Counter.register(import.meta.url)`; the call
is just `Counter.register()`. Module URLs (used by SSR to emit
`<link rel=modulepreload>` hints) are derived server-side at boot by
scanning the app tree for `class X extends WebComponent { static tag =
'...' }` declarations and priming the core registry.
Same DX, same SSR behaviour, one less piece of boilerplate — and the
awkward `import.meta.url` string no longer leaks into user code.
BREAKING — pre-launch, no back-compat shim:
• `WebComponent.register()` no longer accepts any argument.
• `registry.register(tag, cls)` no longer accepts a third moduleUrl arg.
• Added `registry.primeModuleUrl(tag, moduleUrl)` for server-side use.
Components / scaffold
• All 11 blog components migrated (counter, error-card, theme-toggle,
auth-forms, new-post, comments-thread, chat-box, 4 test fixtures).
• Scaffold theme-toggle template migrated.
• CONVENTIONS.md / cursorrules / copilot-instructions / AGENTS.md
templates migrated.
Server
• New packages/server/src/component-scanner.js — regex-based walker
that finds component classes in the app tree, derives
browser-visible URLs, and primes the core registry. Balanced-brace
parsing handles nested methods / objects inside class bodies.
• Wired into createRequestHandler boot and into rebuild(), so tags
added / renamed during dev-server sessions stay up to date.
• `webjs check`'s `components-have-register` rule updated to accept
`.register()` (no args).
Docs
• AGENTS.md, README.md, and 14 docs/ pages updated.
• Editor setup / Styling / Components / Getting Started / Lazy
Loading / Context / Controllers / Task / WebSockets / SSR /
Server Actions / Conventions pages + doc-shell / doc-search /
theme-toggle component sources.
Tests
• New test/component-scanner.test.js — 6 unit tests:
extractComponents shape matching, hyphen requirement, multi-class
files, full directory scan, skip rules (.server.ts, .test.ts,
node_modules), priming verification via lookupModuleUrl.
• test/check.test.js fixtures migrated.
• Full suite: 266 unit / 21 browser, all green.
@@ -802,7 +802,7 @@ When you mark an action as `expose('METHOD /path', fn)`, you are declaring it pa
802
802
803
803
### Components (`components/*.js`)
804
804
805
-
- Each file should define **one** custom element and call `Class.register(import.meta.url)` at module top level.
805
+
- Each file should define **one** custom element and call `Class.register()` at module top level.
806
806
Passing `import.meta.url` lets the SSR shell emit a `<link rel="modulepreload">` so the browser can fetch the module without waiting for its parent to parse. Zero build step; big first-paint win.
807
807
- Imported by pages (for SSR) and/or other components (for composition).
808
808
- **Styling convention: shadow-DOM CSS via `static styles = css\`…\``, not inline `style="…"` attributes.** Any repeated visual chunk in pages (layout chrome, cards, muted labels, etc.) should become a component whose styles live in its shadow root. The example app's `<blog-shell>` and `<muted-text>` demonstrate this — pages emit semantic HTML with zero inline styles.
@@ -189,7 +189,7 @@ class StyledCard extends WebComponent {
189
189
\`;
190
190
}
191
191
}
192
-
StyledCard.register(import.meta.url);</pre>
192
+
StyledCard.register();</pre>
193
193
194
194
<h3>How Styles Are Applied</h3>
195
195
<ul>
@@ -243,7 +243,7 @@ static styles = css\`
243
243
\`;
244
244
}
245
245
}
246
-
AppCard.register(import.meta.url);</pre>
246
+
AppCard.register();</pre>
247
247
248
248
<h3>Class-prefix rule for custom CSS</h3>
249
249
<p>Tailwind utilities are unique by construction, so most light-DOM components need zero custom CSS. If you <em>do</em> author a <code><style></code> block or import a stylesheet, <strong>every class selector MUST be prefixed with the component's tag name</strong>. Otherwise two components with a <code>.card</code> or <code>.header</code> class will style each other.</p>
@@ -299,7 +299,7 @@ class MyCard extends WebComponent {
299
299
\`;
300
300
}
301
301
}
302
-
Card.register(import.meta.url);</pre>
302
+
Card.register();</pre>
303
303
304
304
<p><code>static styles</code> on a light-DOM component is silently ignored — there's no shadow root to adopt them into. If you see your styles failing, check whether you forgot <code>static shadow = true</code>.</p>
305
305
@@ -371,7 +371,7 @@ html\`
371
371
\`;
372
372
}
373
373
}
374
-
PageLayout.register(import.meta.url);
374
+
PageLayout.register();
375
375
376
376
// Usage: assign content to named slots with the slot="" attribute
377
377
html\`
@@ -491,10 +491,10 @@ render() {
491
491
492
492
<p>During SSR, <code>?disabled=\${true}</code> emits <code>disabled=""</code> and <code>?disabled=\${false}</code> emits nothing — matching how the browser interprets boolean attributes.</p>
493
493
494
-
<h2>register(import.meta.url)</h2>
494
+
<h2>register()</h2>
495
495
<p>Every component must call <code>register()</code> after its class definition. This static method does two things:</p>
496
496
497
-
<pre>MyCounter.register(import.meta.url);</pre>
497
+
<pre>MyCounter.register();</pre>
498
498
499
499
<ol>
500
500
<li><strong>Registers with <code>customElements.define()</code></strong> — on the browser, this tells the browser to upgrade all <code><my-counter></code> elements with the <code>MyCounter</code> class. On the server, it stores the class in an internal registry so <code>renderToString</code> can look it up.</li>
@@ -504,7 +504,7 @@ render() {
504
504
<p>You can omit <code>import.meta.url</code> and just call <code>MyCounter.register()</code>, but you lose the modulepreload optimization.</p>
505
505
506
506
<pre>// With module URL — recommended
507
-
Counter.register(import.meta.url);
507
+
Counter.register();
508
508
509
509
// Without module URL — works but no modulepreload hint
510
510
Counter.register();</pre>
@@ -553,7 +553,7 @@ Counter.register();</pre>
553
553
\`;
554
554
}
555
555
}
556
-
UserProfile.register(import.meta.url);</pre>
556
+
UserProfile.register();</pre>
557
557
558
558
<p>On the client, <code>render()</code> is called synchronously. If you need async data on the client, fetch it in <code>connectedCallback()</code> and call <code>setState()</code> when the data arrives.</p>
559
559
@@ -622,7 +622,7 @@ class TaskList extends WebComponent {
622
622
\`;
623
623
}
624
624
}
625
-
TaskList.register(import.meta.url);</pre>
625
+
TaskList.register();</pre>
626
626
627
627
<h3>How repeat() Works</h3>
628
628
<ul>
@@ -701,13 +701,13 @@ class ChatBox extends WebComponent {
701
701
\`;
702
702
}
703
703
}
704
-
ChatBox.register(import.meta.url);</pre>
704
+
ChatBox.register();</pre>
705
705
706
706
<h2>Quick Reference</h2>
707
707
<ul>
708
708
<li><strong>Extend</strong><code>WebComponent</code> and set <code>static tag</code>, <code>static properties</code>, <code>static styles</code>.</li>
<li><strong>Register</strong> with <code>ClassName.register(import.meta.url)</code>.</li>
710
+
<li><strong>Register</strong> with <code>ClassName.register()</code>.</li>
711
711
<li><strong>State</strong> — use <code>this.setState({...})</code> for shallow merge + batched re-render.</li>
712
712
<li><strong>Events</strong> — <code>@click</code>, <code>@submit</code>, <code>@input</code> in templates. Stable dispatchers, no listener churn.</li>
713
713
<li><strong>Bindings</strong> — <code>attr=\${v}</code> for attributes, <code>.prop=\${v}</code> for properties, <code>?bool=\${v}</code> for booleans.</li>
Copy file name to clipboardExpand all lines: docs/app/docs/conventions/page.ts
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -26,7 +26,7 @@ export default function Conventions() {
26
26
27
27
- Opt in to shadow DOM (static shadow = true) for every component
28
28
- Author styles via static styles = css`...`
29
-
- Always call register(import.meta.url)</pre>
29
+
- Always call register()</pre>
30
30
31
31
<p>AI agents read <code>CONVENTIONS.md</code> before every task and follow the overrides. You can also disable specific convention rules in <code>package.json</code>:</p>
<p>Inside the class, <code>this.student</code> is a real <code>Student</code> — hover, autocomplete, type-checking all work. <code>this.setState</code>, <code>this.state</code>, <code>this.requestUpdate</code>, and all lifecycle hooks are typed by the framework's <code>.d.ts</code> overlay.</p>
Copy file name to clipboardExpand all lines: docs/app/docs/server-actions/page.ts
+2-2Lines changed: 2 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -112,7 +112,7 @@ export class PostForm extends WebComponent {
112
112
\`;
113
113
}
114
114
}
115
-
PostForm.register(import.meta.url);</pre>
115
+
PostForm.register();</pre>
116
116
117
117
<h2>The superjson Wire Format</h2>
118
118
<p>Server actions use <strong>superjson</strong> for serialisation, not plain <code>JSON.stringify</code>. The content type is <code>application/vnd.webjs+json</code>. This means rich JavaScript types survive the round trip:</p>
@@ -323,7 +323,7 @@ export class TodoApp extends WebComponent {
323
323
\`;
324
324
}
325
325
}
326
-
TodoApp.register(import.meta.url);
326
+
TodoApp.register();
327
327
328
328
// 3. Call the REST endpoint from curl
329
329
// curl -X POST http://localhost:3000/api/todos \\
0 commit comments