@escape-game-over/atlas 0.1.19 → 0.1.21

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.
@@ -41,7 +41,7 @@ finding them.
41
41
  | `./astro/youtube` | the swap from poster to player, on the click and not before | the poster, the button, the iframe's classes |
42
42
  | `./astro/background-video` | playing or paused, playable or not, and which cut is loaded | the play/pause control, its icons and its label |
43
43
  | `./astro/consent` | remembering an answer, expiring it, handing it to Google | the banner — its wording, its buttons, its law |
44
- | `./astro/element` | the two lifetimes a custom element has, and one abort signal | what the element is and does |
44
+ | `./astro/element` | registering a custom element, and ending what it started | what the element does while it is on the page |
45
45
  | `./astro/dom` | scoped `one`/`all` lookups, typed | the selectors |
46
46
  | `./astro/dev-log` | a panel a failed wiring can announce itself in, in dev only | calling it behind `import.meta.env.DEV` |
47
47
 
@@ -54,6 +54,12 @@ from the package root is the same question at build time: false, and the banner
54
54
  never reaches the HTML. Gate the render on that and keep the runtime check for
55
55
  the banner that is rendered. See NOT-BUILT.md on where the halves divide.
56
56
 
57
+ **A script imports one path: `@escape-game-over/atlas/client`**, which re-exports
58
+ every module above. All of it exists to touch a live document, so it belongs in
59
+ a `<script src>` and not in frontmatter or `astro.config.ts`. A component's
60
+ contract is the one thing both halves need, and it stays on `./astro/markup`,
61
+ which touches nothing.
62
+
57
63
  ## Lifetimes are the recurring bug
58
64
 
59
65
  Every module here that binds a listener hands back the undo, and the undo is
@@ -69,15 +75,23 @@ const detach = loop.attach(video); // background-video: the <video>
69
75
  const detach = trailer.attach(button, box); // youtube: what is pressed, what it replaces
70
76
  ```
71
77
 
72
- `AtlasElement` says the same thing in the shape a custom element needs, because
73
- an element is constructed once and may be connected many times — moving it in
74
- the DOM runs `disconnectedCallback` and then `connectedCallback` again:
78
+ `defineElement` says the same thing in the shape a custom element needs. One
79
+ function per element: it runs when the element enters the page, `signal` ends
80
+ what it registered when the element leaves, and what it returns is the undo for
81
+ everything else.
75
82
 
76
83
  ```ts
77
- protected setup(): void { … } // once, ever: what the element is
78
- protected connect(): void { } // every connection: bind with this.signal
84
+ defineElement("atlas-thing", (host, signal) => {
85
+ host.addEventListener("click", open, { signal }); // ends with the visit
86
+ return loop.attach(host); // and so does this
87
+ });
79
88
  ```
80
89
 
90
+ An element can enter the page more than once — moving it in the DOM runs the
91
+ undo and then the function again — so it has to be able to run twice. State that
92
+ must survive a move goes in a `WeakMap` keyed by `host`, which is what the
93
+ carousel example does with its index.
94
+
81
95
  **The trap this exists for is view transitions.** A bundled `<script src>` is an
82
96
  ES module, cached by URL, so it executes once per session — not once per
83
97
  navigation. Bind at module scope with `ClientRouter` on and the incoming page
@@ -408,9 +422,24 @@ const input = search.require<HTMLInputElement>(this).element;
408
422
  a list that quietly loses a row is the failure this exists to prevent.
409
423
 
410
424
  The kinds are data, not builders, for the reason `filters` gives:
411
- `text`, `number`, `list`, `choice` and `flag`, the first four optionally
412
- `optional`. A `flag` is off by being absent, never `"false"`, so a stylesheet can
413
- select it with a bare attribute.
425
+ `text`, `number`, `list`, `template`, `choice` and `flag`, all but `flag`
426
+ optionally `optional`. A `flag` is off by being absent, never `"false"`, so a
427
+ stylesheet can select it with a bare attribute.
428
+
429
+ A `template` is the one that keeps a sentence out of the script. It is written
430
+ as the translated string with its placeholders left in, and read back as the
431
+ function that fills them:
432
+
433
+ ```astro
434
+ <p {...faq.line.attrs({ sentence: t("faq.matched", { count: "{count}" }) })}></p>
435
+ ```
436
+
437
+ ```ts
438
+ line.element.textContent = line.values.sentence({ count: String(hits.size) });
439
+ ```
440
+
441
+ So no script spells `"{count}"`, and a sentence that never says one of its own
442
+ placeholders fails the build rather than shipping a brace to a reader.
414
443
 
415
444
  `write` sets fields back onto an element, for state a stylesheet reads — a
416
445
  dropdown that is blocked — so the script uses the names the template did.
@@ -450,12 +479,9 @@ export const faq = component("go-faq", {
450
479
  ```
451
480
 
452
481
  ```ts
453
- class Faq extends AtlasElement {
454
- protected connect(): void {
455
- for (const { element, values } of faq.row.all(this)) { … }
456
- }
457
- }
458
- faq.define(Faq);
482
+ defineElement(faq.tag, (host, signal) => {
483
+ for (const { element, values } of faq.row.all(host)) { … }
484
+ });
459
485
  ```
460
486
 
461
487
  - **Names come from the tag.** Every attribute is `data-<tag>-<role>` plus the
@@ -468,18 +494,22 @@ faq.define(Faq);
468
494
  keeps its elements, and a lookup from an element inside the instance, like a
469
495
  dropdown's own panel, still counts as that instance. Elements are filtered
470
496
  before they are read, so a broken nested copy cannot fail the outer lookup.
471
- - **The tag is written once.** `faq.tag` is what the template renders and
472
- `faq.define` registers the class under it.
497
+ - **The tag is written once.** `faq.tag` is what the template renders and what
498
+ `defineElement` registers the behaviour under. A contract stays inert either
499
+ way: it names things, and nothing in it touches a document, which is why
500
+ frontmatter can import it.
473
501
  - **The names are checked in the editor.** A tag without a hyphen, or a role
474
502
  that is not camelCase, is a compile error where it is written — the rules are
475
503
  types, character by character, as in `i18n/placeholders.ts`. Nothing is
476
504
  validated at run time, because a name that reached the browser malformed would
477
505
  already have been refused there: by `setAttribute`, by `querySelectorAll`, or
478
506
  by `customElements.define`.
507
+ - **Most roles carry nothing**, and say so: `search: marker` rather than
508
+ `search: {}`, which reads like options somebody forgot to fill in.
479
509
 
480
- `tag` and `define` are the component's own keys, so no role may take them. A role
481
- that lives outside every instance, such as a footer button that reopens a
482
- banner, is still plain `markup`.
510
+ `tag` is the component's own key, so no role may take it. A role that lives
511
+ outside every instance, such as a footer button that reopens a banner, is still
512
+ plain `markup`.
483
513
 
484
514
  ## `youtube`
485
515
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -17,6 +17,7 @@
17
17
  "./astro/images": "./src/astro/images.ts",
18
18
  "./astro/background-video": "./src/astro/background-video.ts",
19
19
  "./astro/carousel": "./src/astro/carousel.ts",
20
+ "./client": "./src/astro/client.ts",
20
21
  "./astro/consent": "./src/astro/consent.ts",
21
22
  "./astro/dev-log": "./src/astro/dev-log.ts",
22
23
  "./astro/dom": "./src/astro/dom.ts",
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Everything a client script needs, from one import.
3
+ *
4
+ * ```ts
5
+ * import { filters, searchBox } from "@escape-game-over/atlas/client";
6
+ * ```
7
+ *
8
+ * **For the browser.** Everything here exists to touch a live document, and is
9
+ * meant for a `<script src>`, not for frontmatter or `astro.config.ts`. A
10
+ * component's contract is the one thing both halves need, and it stays on
11
+ * `astro/markup`, which touches nothing.
12
+ */
13
+
14
+ export * from "./background-video.ts";
15
+ export * from "./carousel.ts";
16
+ export * from "./consent.ts";
17
+ export * from "./dev-log.ts";
18
+ export * from "./dom.ts";
19
+ export * from "./element.ts";
20
+ export * from "./filters.ts";
21
+ export * from "./filters-view.ts";
22
+ export * from "./markup.ts";
23
+ export * from "./youtube.ts";
@@ -1,181 +1,112 @@
1
1
  import { reportDevError } from "./dev-log.ts";
2
- import { within } from "./dom.ts";
3
2
 
4
3
  /**
5
- * A base for custom elements: two lifetimes, and one abort signal.
4
+ * What is left to undo when the element leaves, or nothing.
5
+ *
6
+ * Nothing is the common case — a connect that registered everything with
7
+ * `signal` has already said how it comes down — so `void` rather than
8
+ * `undefined`, which would make every connect end in a `return`.
9
+ */
10
+ // biome-ignore lint/suspicious/noConfusingVoidType: that is the distinction.
11
+ type Undo = void | (() => void);
12
+
13
+ /**
14
+ * What an element does while it is on the page.
15
+ *
16
+ * `host` is the element itself and `signal` is aborted when it leaves, so
17
+ * anything registered with `signal` comes down on its own. Whatever else has to
18
+ * be undone is the returned function, which runs at the same moment.
19
+ */
20
+ export type Connect = (host: HTMLElement, signal: AbortSignal) => Undo;
21
+
22
+ /**
23
+ * Registers a custom element whose behaviour is one function.
6
24
  *
7
25
  * ```ts
8
- * class Thing extends AtlasElement {
9
- * protected setup(): void { } // once, ever: what the element is
10
- * protected connect(): void { // every connection: bind with signal
11
- * this.require("form").addEventListener("submit", send, {
12
- * signal: this.signal,
13
- * });
14
- * }
15
- * }
16
- * customElements.define("atlas-thing", Thing);
26
+ * defineElement("atlas-thing", (host, signal) => {
27
+ * host.querySelector("form")?.addEventListener("submit", send, { signal });
28
+ * return loop.attach(host); // runs when the element leaves the page
29
+ * });
17
30
  * ```
18
31
  *
19
- * - **An element is constructed once and connected many times.** Moving it in
20
- * the DOM runs `disconnectedCallback` then `connectedCallback` again, so
21
- * state belongs in `setup` and listeners in `connect`. Anything registered
22
- * with `signal` is dropped on disconnect and rebound on the next connect.
23
- * - **Override `setup`, `connect` and `disconnect`, never the callbacks.**
24
- * Overriding `connectedCallback` skips the controller and leaks every
25
- * listener; that mistake throws in development, below.
32
+ * - **An element can enter the page more than once.** Moving it runs the undo
33
+ * and then `connect` again, so `connect` has to be able to run twice. State
34
+ * that must survive a move belongs in a `WeakMap` keyed by `host`, as the
35
+ * carousel example keeps its index.
26
36
  * - **The defining script must stay a deferred module.** Astro emits
27
- * `<script src>` as `type="module"`, so the element has its children when it
28
- * is upgraded. `is:inline` runs it too early and every lookup finds nothing.
29
- * - **`attributeChangedCallback` runs before `connectedCallback`**, where
30
- * `signal` is not readable yet. Record what changed and act on it in
31
- * `connect`.
32
- * - **`disconnect` is not a destructor.** A move fires it and connects again a
33
- * moment later, so it is connection teardown and nothing else.
37
+ * `<script src>` as `type="module"`, so the element has its children by the
38
+ * time it is upgraded. `is:inline` runs it too early and every lookup inside
39
+ * the element finds nothing.
40
+ * - **A `connect` that throws leaves that one element inert** and reports it,
41
+ * rather than taking its siblings down with it — which is what lets a lookup
42
+ * like `require` throw and say what is missing.
34
43
  *
35
44
  * See docs/client-scripts.md.
36
45
  */
37
- export abstract class AtlasElement extends HTMLElement {
38
- /**
39
- * This connection's listeners, and only this connection's. Remade on every
40
- * connect: an `AbortController` is single-use, and a reused one comes back
41
- * already aborted, leaving the element inert but normal-looking.
42
- */
43
- #ac?: AbortController;
44
-
45
- #ready = false;
46
-
47
- constructor() {
48
- super();
49
- // The bare expression Vite substitutes, so this collapses to
50
- // `if (false)` and the method below leaves the bundle.
51
- if (import.meta.env.DEV) this.#assertHooks();
52
- }
53
-
54
- /**
55
- * Refuses a subclass that overrode the wrong lifecycle method. An override
56
- * is an *own* property of the subclass prototype; the loop covers an
57
- * intermediate base class that got it wrong.
58
- */
59
- #assertHooks(): void {
60
- // A tuple array, not an object: `Object.entries` would widen the keys
61
- // back to `string` and stop checking the pairing.
62
- const wrong = [
63
- ["connectedCallback", "connect"],
64
- ["disconnectedCallback", "disconnect"],
65
- ] as const;
66
-
67
- for (
68
- let proto = Object.getPrototypeOf(this);
69
- proto && proto !== AtlasElement.prototype;
70
- proto = Object.getPrototypeOf(proto)
71
- ) {
72
- for (const [callback, hook] of wrong) {
73
- if (Object.hasOwn(proto, callback)) {
74
- const error = new Error(
75
- `override "${hook}", not "${callback}" — see AtlasElement`
76
- );
77
- // Reported before throwing: this runs during upgrade, where
78
- // a throw would only surface as an uncaught console error.
79
- reportDevError(this.constructor.name, error);
80
- throw error;
46
+ export function defineElement(tag: string, connect: Connect): void {
47
+ // The class is built in here rather than at module scope so `HTMLElement`
48
+ // is only read in a browser: `astro/markup` reaches this file, templates
49
+ // import it, and the build runs in Node.
50
+ customElements.define(
51
+ tag,
52
+ class extends HTMLElement {
53
+ /**
54
+ * This visit's listeners, and only this visit's. Remade every time:
55
+ * an `AbortController` is single-use, and a reused one comes back
56
+ * already aborted, leaving the element inert but normal-looking.
57
+ */
58
+ #ac?: AbortController;
59
+
60
+ /** What `connect` handed back, if anything. */
61
+ #undo?: () => void;
62
+
63
+ connectedCallback(): void {
64
+ // Insurance: a live controller still here would orphan its
65
+ // listeners.
66
+ this.#end();
67
+
68
+ const ac = new AbortController();
69
+ this.#ac = ac;
70
+ try {
71
+ this.#undo = connect(this, ac.signal) ?? undefined;
72
+ } catch (error) {
73
+ this.#report(error);
81
74
  }
82
75
  }
83
- }
84
- }
85
-
86
- /**
87
- * Pass to `addEventListener`, observers, anything that should stop when the
88
- * element leaves the document. Throws outside a connection.
89
- */
90
- protected get signal(): AbortSignal {
91
- if (!this.#ac) {
92
- throw new Error(
93
- `${this.localName}: signal read outside a connection`
94
- );
95
- }
96
- return this.#ac.signal;
97
- }
98
-
99
- connectedCallback(): void {
100
- // Insurance: overwriting a live controller would orphan its listeners.
101
- this.#ac?.abort();
102
- this.#ac = new AbortController();
103
76
 
104
- // Caught so one broken element logs and sits inert rather than taking
105
- // its siblings with it — which is what lets `require` throw.
106
- try {
107
- if (!this.#ready) {
108
- this.setup();
109
- // Only once it returned, so a `setup` that threw is retried
110
- // rather than leaving `connect` to run against nothing.
111
- this.#ready = true;
77
+ disconnectedCallback(): void {
78
+ this.#end();
112
79
  }
113
- this.connect();
114
- } catch (error) {
115
- this.#report(error);
116
- }
117
- }
118
-
119
- disconnectedCallback(): void {
120
- // Idempotent, so `adoptedCallback` can delegate here.
121
- if (!this.#ac) return;
122
- this.#ac.abort();
123
-
124
- try {
125
- // Before `#ac` is cleared, so `this.signal` is readable and already
126
- // aborted: an async continuation can check it and bail.
127
- this.disconnect();
128
- } catch (error) {
129
- this.#report(error);
130
- }
131
-
132
- this.#ac = undefined;
133
- }
134
-
135
- /** Moving to another document ends the old document's connection. */
136
- adoptedCallback(): void {
137
- this.disconnectedCallback();
138
- }
139
-
140
- #report(error: unknown): void {
141
- if (import.meta.env.DEV) {
142
- reportDevError(this.localName, error);
143
- return;
144
- }
145
- console.error(`${this.localName}:`, error);
146
- }
147
80
 
148
- /** Runs once per element, before its first `connect`. State belongs here. */
149
- protected setup(): void {}
150
-
151
- /** Runs on every connect, with `signal` and the children both available. */
152
- protected abstract connect(): void;
153
-
154
- /** Runs on every disconnect, once the signal has been aborted. */
155
- protected disconnect(): void {}
156
-
157
- /** The first match inside this element, or `null`. */
158
- protected one<T extends Element = HTMLElement>(selector: string): T | null {
159
- return within(this).one<T>(selector);
160
- }
81
+ /** Moving to another document ends the old document's visit. */
82
+ adoptedCallback(): void {
83
+ this.#end();
84
+ }
161
85
 
162
- /** Every match inside this element, as an array. */
163
- protected all<T extends Element = HTMLElement>(selector: string): T[] {
164
- return within(this).all<T>(selector);
165
- }
86
+ #end(): void {
87
+ // Aborted before the undo runs, so an async continuation that
88
+ // checks the signal can see the visit is over.
89
+ this.#ac?.abort();
90
+ this.#ac = undefined;
91
+
92
+ const undo = this.#undo;
93
+ this.#undo = undefined;
94
+ try {
95
+ undo?.();
96
+ } catch (error) {
97
+ this.#report(error);
98
+ }
99
+ }
166
100
 
167
- /**
168
- * The first match, or a thrown error naming what was missing. Throws rather
169
- * than returning `null`, because the `?.` at every call site swallows the
170
- * failure as thoroughly as the missing element did.
171
- */
172
- protected require<T extends Element = HTMLElement>(selector: string): T {
173
- const found = this.one<T>(selector);
174
- if (!found) {
175
- throw new Error(
176
- `nothing matched "${selector}" — is the script still a deferred module?`
177
- );
101
+ #report(error: unknown): void {
102
+ // The bare expression Vite substitutes, so this collapses to
103
+ // `if (false)` and the dev panel leaves the bundle.
104
+ if (import.meta.env.DEV) {
105
+ reportDevError(this.localName, error);
106
+ return;
107
+ }
108
+ console.error(`${this.localName}:`, error);
109
+ }
178
110
  }
179
- return found;
180
- }
111
+ );
181
112
  }
@@ -123,6 +123,9 @@ type CheckedKeys<T> = {
123
123
  * - `text` — any string, the empty one included.
124
124
  * - `number` — finite only.
125
125
  * - `list` — several words, space-separated, as `class` is.
126
+ * - `template` — a sentence with `{placeholders}` in it, read back as the
127
+ * function that fills them. A template that never says one of its own
128
+ * `params` fails the build, rather than shipping a brace to a reader.
126
129
  * - `choice` — one of `of`, checked at compile time and again when read.
127
130
  * - `flag` — present or absent. Off is the attribute left out, never `"false"`,
128
131
  * so a stylesheet can select it with a bare `[data-…]`.
@@ -134,6 +137,11 @@ export type MarkupField =
134
137
  | { readonly kind: "text"; readonly optional?: boolean }
135
138
  | { readonly kind: "number"; readonly optional?: boolean }
136
139
  | { readonly kind: "list"; readonly optional?: boolean }
140
+ | {
141
+ readonly kind: "template";
142
+ readonly params: readonly [string, ...string[]];
143
+ readonly optional?: boolean;
144
+ }
137
145
  | {
138
146
  readonly kind: "choice";
139
147
  readonly of: readonly [string, ...string[]];
@@ -144,16 +152,36 @@ export type MarkupField =
144
152
  /** The fields of one role, named by the caller. */
145
153
  export type MarkupFields = Readonly<Record<string, MarkupField>>;
146
154
 
147
- /** The value a field holds when present. */
155
+ /**
156
+ * A role that carries nothing: `search: marker`, where `search: {}` reads like
157
+ * options somebody forgot to fill in. Most roles are these — they name an
158
+ * element the script has to find, and hold no values.
159
+ */
160
+ export const marker = {} as const;
161
+
162
+ /** What `attrs` takes for a field, and what lands in the attribute. */
148
163
  type Held<F extends MarkupField> = F extends { kind: "text" }
149
164
  ? string
150
165
  : F extends { kind: "number" }
151
166
  ? number
152
167
  : F extends { kind: "list" }
153
168
  ? readonly string[]
154
- : F extends { kind: "choice"; of: readonly (infer V)[] }
155
- ? V
156
- : boolean;
169
+ : F extends { kind: "template" }
170
+ ? string
171
+ : F extends { kind: "choice"; of: readonly (infer V)[] }
172
+ ? V
173
+ : boolean;
174
+
175
+ /**
176
+ * What a script reads back — the same, except that a `template` arrives as the
177
+ * function that fills it, so no script spells a placeholder.
178
+ */
179
+ type Read<F extends MarkupField> = F extends {
180
+ kind: "template";
181
+ params: readonly (infer P)[];
182
+ }
183
+ ? (values: Record<P & string, string>) => string
184
+ : Held<F>;
157
185
 
158
186
  /** Whether a template may leave the field out. */
159
187
  type Omittable<F extends MarkupField> = F extends { kind: "flag" }
@@ -164,8 +192,8 @@ type Omittable<F extends MarkupField> = F extends { kind: "flag" }
164
192
 
165
193
  /** One field's value as a script reads it. */
166
194
  export type MarkupValue<F extends MarkupField> = F extends { optional: true }
167
- ? Held<F> | undefined
168
- : Held<F>;
195
+ ? Read<F> | undefined
196
+ : Read<F>;
169
197
 
170
198
  /** Every field's value, as a script reads it. */
171
199
  export type MarkupValues<F extends MarkupFields> = {
@@ -281,6 +309,19 @@ function encode(
281
309
  return typeof value === "string" && field.of.includes(value)
282
310
  ? value
283
311
  : refuse(`expected one of ${field.of.join(", ")}`);
312
+ case "template": {
313
+ if (typeof value !== "string") {
314
+ return refuse("it is a template field");
315
+ }
316
+ for (const param of field.params) {
317
+ if (!value.includes(`{${param}}`)) {
318
+ refuse(
319
+ `it never says {${param}}, so that value would have nowhere to go`
320
+ );
321
+ }
322
+ }
323
+ return value;
324
+ }
284
325
  case "list": {
285
326
  if (!Array.isArray(value)) return refuse("it is a list field");
286
327
  for (const item of value) {
@@ -331,6 +372,13 @@ function decode(
331
372
  return raw;
332
373
  case "list":
333
374
  return raw.split(/\s+/).filter((item) => item !== "");
375
+ case "template":
376
+ return (values: Record<string, string>) =>
377
+ field.params.reduce(
378
+ (text, param) =>
379
+ text.replaceAll(`{${param}}`, values[param] ?? ""),
380
+ raw
381
+ );
334
382
  }
335
383
  }
336
384
 
@@ -466,14 +514,13 @@ export function markup<
466
514
  /** A component's roles, by name: each is the fields of one `markup` role. */
467
515
  export type ComponentRoles = Readonly<Record<string, MarkupFields>>;
468
516
 
469
- /** Keys the component object uses itself, so no role may take them. */
470
- type Reserved = "tag" | "define";
517
+ /** The key the component object uses itself, so no role may take it. */
518
+ type Reserved = "tag";
471
519
 
472
520
  export type Component<Tag extends string, R extends ComponentRoles> = {
473
- /** The custom element's name: what the template renders. */
521
+ /** The custom element's name: what the template renders, and what
522
+ * `defineElement` registers the behaviour under. */
474
523
  readonly tag: Tag;
475
- /** Registers the element's class under `tag`. */
476
- define(element: CustomElementConstructor): void;
477
524
  } & { readonly [K in keyof R]: Markup<R[K]> };
478
525
 
479
526
  /**
@@ -487,7 +534,7 @@ export type Component<Tag extends string, R extends ComponentRoles> = {
487
534
  * });
488
535
  *
489
536
  * // template: <faq.tag> … <details {...faq.row.attrs({ key, text })}>
490
- * // script: faq.row.all(this); faq.define(Faq);
537
+ * // script: defineElement(faq.tag, (host) => faq.row.all(host).forEach());
491
538
  * ```
492
539
  *
493
540
  * Two things `markup` alone leaves to the project:
@@ -531,9 +578,9 @@ export function component<
531
578
  name: string,
532
579
  fields: MarkupFields
533
580
  ): Markup<MarkupFields> => {
534
- // A compile error already, and cheap to keep: these two would overwrite
535
- // the component's own keys and leave nothing to define the element with.
536
- if (name === "tag" || name === "define") {
581
+ // A compile error already, and cheap to keep: this one would overwrite
582
+ // the component's own key and leave nothing to render the element as.
583
+ if (name === "tag") {
537
584
  throw new Error(
538
585
  `component(${show(tag)}): ${show(name)} is taken by the component itself`
539
586
  );
@@ -583,11 +630,7 @@ export function component<
583
630
  };
584
631
  };
585
632
 
586
- const built: Record<string, unknown> = {
587
- tag,
588
- define: (element: CustomElementConstructor) =>
589
- customElements.define(tag, element),
590
- };
633
+ const built: Record<string, unknown> = { tag };
591
634
  for (const [name, fields] of Object.entries(roles)) {
592
635
  built[name] = scoped(name, fields);
593
636
  }