Skip to content

Commit 283c2b5

Browse files
committed
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.
1 parent c102f4d commit 283c2b5

45 files changed

Lines changed: 444 additions & 90 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ class MyThing extends WebComponent {
432432
return html``;
433433
}
434434
}
435-
MyThing.register(import.meta.url);
435+
MyThing.register();
436436
```
437437

438438
Mutate state with `this.setState({...})` — it batches a re-render via microtask.
@@ -458,7 +458,7 @@ class StudentCard extends WebComponent {
458458
return html`<p>${this.student.name}</p>`;
459459
}
460460
}
461-
StudentCard.register(import.meta.url);
461+
StudentCard.register();
462462
```
463463

464464
Built-in constructors (`String`, `Number`, `Boolean`, `Array`, `Object`)
@@ -802,7 +802,7 @@ When you mark an action as `expose('METHOD /path', fn)`, you are declaring it pa
802802
803803
### Components (`components/*.js`)
804804
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.
806806
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.
807807
- Imported by pages (for SSR) and/or other components (for composition).
808808
- **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.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export class Counter extends WebComponent {
100100
`;
101101
}
102102
}
103-
Counter.register(import.meta.url);
103+
Counter.register();
104104
```
105105

106106
Need scoped styles, `<slot>` projection, or embed-ready isolation? Opt

docs/app/docs/components/page.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class MyCounter extends WebComponent {
3636
}
3737
}
3838
39-
MyCounter.register(import.meta.url);</pre>
39+
MyCounter.register();</pre>
4040
4141
<p>That is a complete, working component. Import it from a page or layout and use it like any HTML element:</p>
4242
@@ -85,7 +85,7 @@ export default function Home() {
8585
\`;
8686
}
8787
}
88-
UserCard.register(import.meta.url);</pre>
88+
UserCard.register();</pre>
8989
9090
<h3>Attribute-to-Property Coercion</h3>
9191
<p>When an attribute changes on the DOM element, webjs coerces the string value to the declared type:</p>
@@ -146,7 +146,7 @@ UserCard.register(import.meta.url);</pre>
146146
\`;
147147
}
148148
}
149-
TodoList.register(import.meta.url);</pre>
149+
TodoList.register();</pre>
150150
151151
<h3>How setState Works</h3>
152152
<ul>
@@ -189,7 +189,7 @@ class StyledCard extends WebComponent {
189189
\`;
190190
}
191191
}
192-
StyledCard.register(import.meta.url);</pre>
192+
StyledCard.register();</pre>
193193
194194
<h3>How Styles Are Applied</h3>
195195
<ul>
@@ -243,7 +243,7 @@ static styles = css\`
243243
\`;
244244
}
245245
}
246-
AppCard.register(import.meta.url);</pre>
246+
AppCard.register();</pre>
247247
248248
<h3>Class-prefix rule for custom CSS</h3>
249249
<p>Tailwind utilities are unique by construction, so most light-DOM components need zero custom CSS. If you <em>do</em> author a <code>&lt;style&gt;</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 {
299299
\`;
300300
}
301301
}
302-
Card.register(import.meta.url);</pre>
302+
Card.register();</pre>
303303
304304
<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>
305305
@@ -371,7 +371,7 @@ html\`
371371
\`;
372372
}
373373
}
374-
PageLayout.register(import.meta.url);
374+
PageLayout.register();
375375
376376
// Usage: assign content to named slots with the slot="" attribute
377377
html\`
@@ -491,10 +491,10 @@ render() {
491491
492492
<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>
493493
494-
<h2>register(import.meta.url)</h2>
494+
<h2>register()</h2>
495495
<p>Every component must call <code>register()</code> after its class definition. This static method does two things:</p>
496496
497-
<pre>MyCounter.register(import.meta.url);</pre>
497+
<pre>MyCounter.register();</pre>
498498
499499
<ol>
500500
<li><strong>Registers with <code>customElements.define()</code></strong> — on the browser, this tells the browser to upgrade all <code>&lt;my-counter&gt;</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() {
504504
<p>You can omit <code>import.meta.url</code> and just call <code>MyCounter.register()</code>, but you lose the modulepreload optimization.</p>
505505
506506
<pre>// With module URL — recommended
507-
Counter.register(import.meta.url);
507+
Counter.register();
508508
509509
// Without module URL — works but no modulepreload hint
510510
Counter.register();</pre>
@@ -553,7 +553,7 @@ Counter.register();</pre>
553553
\`;
554554
}
555555
}
556-
UserProfile.register(import.meta.url);</pre>
556+
UserProfile.register();</pre>
557557
558558
<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>
559559
@@ -622,7 +622,7 @@ class TaskList extends WebComponent {
622622
\`;
623623
}
624624
}
625-
TaskList.register(import.meta.url);</pre>
625+
TaskList.register();</pre>
626626
627627
<h3>How repeat() Works</h3>
628628
<ul>
@@ -701,13 +701,13 @@ class ChatBox extends WebComponent {
701701
\`;
702702
}
703703
}
704-
ChatBox.register(import.meta.url);</pre>
704+
ChatBox.register();</pre>
705705
706706
<h2>Quick Reference</h2>
707707
<ul>
708708
<li><strong>Extend</strong> <code>WebComponent</code> and set <code>static tag</code>, <code>static properties</code>, <code>static styles</code>.</li>
709709
<li><strong>Implement</strong> <code>render()</code> returning <code>html\`...\`</code>.</li>
710-
<li><strong>Register</strong> with <code>ClassName.register(import.meta.url)</code>.</li>
710+
<li><strong>Register</strong> with <code>ClassName.register()</code>.</li>
711711
<li><strong>State</strong> — use <code>this.setState({...})</code> for shallow merge + batched re-render.</li>
712712
<li><strong>Events</strong><code>@click</code>, <code>@submit</code>, <code>@input</code> in templates. Stable dispatchers, no listener churn.</li>
713713
<li><strong>Bindings</strong><code>attr=\${v}</code> for attributes, <code>.prop=\${v}</code> for properties, <code>?bool=\${v}</code> for booleans.</li>

docs/app/docs/context/page.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class AppShell extends WebComponent {
7171
\`;
7272
}
7373
}
74-
AppShell.register(import.meta.url);</pre>
74+
AppShell.register();</pre>
7575
7676
<p>When you call <code>provider.setValue(newValue)</code>, every subscribed consumer is notified and its host component re-renders automatically.</p>
7777
@@ -106,7 +106,7 @@ class ThemedCard extends WebComponent {
106106
\`;
107107
}
108108
}
109-
ThemedCard.register(import.meta.url);</pre>
109+
ThemedCard.register();</pre>
110110
111111
<h2>Subscribe vs One-Shot Mode</h2>
112112
<p>The <code>subscribe</code> option controls whether the consumer receives ongoing updates:</p>
@@ -178,7 +178,7 @@ class AppRoot extends WebComponent {
178178
return html\`&lt;slot&gt;&lt;/slot&gt;\`;
179179
}
180180
}
181-
AppRoot.register(import.meta.url);</pre>
181+
AppRoot.register();</pre>
182182
183183
<h2>Nested Providers</h2>
184184
<p>Providers can be nested. A consumer resolves to the nearest ancestor provider with a matching context key:</p>
@@ -230,7 +230,7 @@ class AuthProvider extends WebComponent {
230230
return html\`&lt;slot&gt;&lt;/slot&gt;\`;
231231
}
232232
}
233-
AuthProvider.register(import.meta.url);
233+
AuthProvider.register();
234234
235235
// components/user-menu.ts
236236
import { WebComponent, html } from 'webjs';
@@ -252,7 +252,7 @@ class UserMenu extends WebComponent {
252252
return html\`&lt;span&gt;Hi, \${user.name}&lt;/span&gt;\`;
253253
}
254254
}
255-
UserMenu.register(import.meta.url);</pre>
255+
UserMenu.register();</pre>
256256
257257
<h2>Next Steps</h2>
258258
<ul>

docs/app/docs/controllers/page.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class LazyImage extends WebComponent {
7979
\`;
8080
}
8181
}
82-
LazyImage.register(import.meta.url);</pre>
82+
LazyImage.register();</pre>
8383
8484
<h2>Example: FetchController</h2>
8585
<p>A reusable controller that fetches data from a URL and exposes loading/error/data states:</p>
@@ -137,7 +137,7 @@ class UserList extends WebComponent {
137137
\`;
138138
}
139139
}
140-
UserList.register(import.meta.url);</pre>
140+
UserList.register();</pre>
141141
142142
<h2>Multiple Controllers on One Component</h2>
143143
<p>Controllers compose naturally. A single component can use any number of controllers:</p>
@@ -159,7 +159,7 @@ UserList.register(import.meta.url);</pre>
159159
return html\`&lt;div&gt;\${this.#data.data?.summary}&lt;/div&gt;\`;
160160
}
161161
}
162-
DashboardWidget.register(import.meta.url);</pre>
162+
DashboardWidget.register();</pre>
163163
164164
<h2>Built-in Controllers</h2>
165165
<p>webjs ships three controllers out of the box:</p>

docs/app/docs/conventions/page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function Conventions() {
2626
2727
- Opt in to shadow DOM (static shadow = true) for every component
2828
- Author styles via static styles = css`...`
29-
- Always call register(import.meta.url)</pre>
29+
- Always call register()</pre>
3030
3131
<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>
3232

docs/app/docs/editor-setup/page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class StudentCard extends WebComponent {
5757
return html\`&lt;p&gt;\${this.student.name}&lt;/p&gt;\`;
5858
}
5959
}
60-
StudentCard.register(import.meta.url);</pre>
60+
StudentCard.register();</pre>
6161
6262
<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>
6363

docs/app/docs/getting-started/page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export class Counter extends WebComponent {
109109
\`;
110110
}
111111
}
112-
Counter.register(import.meta.url);</pre>
112+
Counter.register();</pre>
113113
114114
<h3>Run it</h3>
115115
<pre>npx webjs dev

docs/app/docs/lazy-loading/page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class HeavyChart extends WebComponent {
3535
return html${'`'}&lt;canvas&gt;&lt;/canvas&gt;${'`'};
3636
}
3737
}
38-
HeavyChart.register(import.meta.url);</pre>
38+
HeavyChart.register();</pre>
3939
4040
<h2>How it works</h2>
4141
<ol>

docs/app/docs/server-actions/page.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export class PostForm extends WebComponent {
112112
\`;
113113
}
114114
}
115-
PostForm.register(import.meta.url);</pre>
115+
PostForm.register();</pre>
116116
117117
<h2>The superjson Wire Format</h2>
118118
<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 {
323323
\`;
324324
}
325325
}
326-
TodoApp.register(import.meta.url);
326+
TodoApp.register();
327327
328328
// 3. Call the REST endpoint from curl
329329
// curl -X POST http://localhost:3000/api/todos \\

0 commit comments

Comments
 (0)