ilha 0.15.2 → 0.16.0

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/oxlint.cjs ADDED
@@ -0,0 +1,596 @@
1
+ const DOCS = "https://ilha.build/guide/recipes/oxlint";
2
+ const PRIMITIVES = new Set(["atom", "watch", "when", "context"]);
3
+ const MEMBER_PRIMITIVES = new Map([
4
+ ["atom", new Set(["lazy"])],
5
+ ["watch", new Set(["once"])],
6
+ ]);
7
+ const INSTRUCTIONS = new Set(["when"]);
8
+ const CALLBACK_PARENTS = new Set([
9
+ "CallExpression",
10
+ "NewExpression",
11
+ "JSXExpressionContainer",
12
+ "Property",
13
+ ]);
14
+ const REACT_EVENT_ALIASES = new Map([["onDoubleClick", "ondblclick"]]);
15
+ const REACT_PROPS = new Map([
16
+ ["className", "class"],
17
+ ["htmlFor", "for"],
18
+ ]);
19
+ const see = (rule) => `See ${DOCS}#${rule}`;
20
+ const CONDITIONAL = new Set([
21
+ "IfStatement",
22
+ "ForStatement",
23
+ "ForInStatement",
24
+ "ForOfStatement",
25
+ "WhileStatement",
26
+ "DoWhileStatement",
27
+ "SwitchStatement",
28
+ "ConditionalExpression",
29
+ "LogicalExpression",
30
+ ]);
31
+ const FN_TYPES = new Set([
32
+ "FunctionDeclaration",
33
+ "FunctionExpression",
34
+ "ArrowFunctionExpression",
35
+ ]);
36
+
37
+ const objectTag = (value) => Object.prototype.toString.call(value);
38
+ const isFunction = (value) => {
39
+ const tag = objectTag(value);
40
+ return (
41
+ tag === "[object Function]" ||
42
+ tag === "[object AsyncFunction]" ||
43
+ tag === "[object GeneratorFunction]"
44
+ );
45
+ };
46
+
47
+ const bindings = () => ({ prim: new Map() });
48
+
49
+ const takeImport = (b, node) => {
50
+ if (node.source.value !== "ilha") {
51
+ return;
52
+ }
53
+ for (const s of node.specifiers) {
54
+ if (s.type !== "ImportSpecifier") {
55
+ continue;
56
+ }
57
+ if (PRIMITIVES.has(s.imported.name)) {
58
+ b.prim.set(s.local.name, s.imported.name);
59
+ }
60
+ }
61
+ };
62
+
63
+ const primitiveName = (node, b) => {
64
+ if (node.type !== "CallExpression") {
65
+ return null;
66
+ }
67
+ const { callee } = node;
68
+ if (callee.type === "Identifier") {
69
+ return b.prim.get(callee.name) ?? null;
70
+ }
71
+ if (
72
+ callee.type === "MemberExpression" &&
73
+ !callee.computed &&
74
+ callee.object.type === "Identifier" &&
75
+ callee.property.type === "Identifier"
76
+ ) {
77
+ const base = b.prim.get(callee.object.name);
78
+ if (base && MEMBER_PRIMITIVES.get(base)?.has(callee.property.name)) {
79
+ return `${base}.${callee.property.name}`;
80
+ }
81
+ }
82
+ return null;
83
+ };
84
+
85
+ const ancestorsOf = (context, node) => {
86
+ const sc = context.sourceCode;
87
+ if (sc && isFunction(sc.getAncestors)) {
88
+ return sc.getAncestors(node);
89
+ }
90
+ return [];
91
+ };
92
+
93
+ const blockBody = (node) => {
94
+ if (node.type === "BlockStatement") {
95
+ return node.body;
96
+ }
97
+ if (
98
+ node.type === "FunctionDeclaration" ||
99
+ node.type === "FunctionExpression" ||
100
+ node.type === "ArrowFunctionExpression"
101
+ ) {
102
+ const { body } = node;
103
+ if (body?.type === "BlockStatement") {
104
+ return body.body;
105
+ }
106
+ }
107
+ return null;
108
+ };
109
+
110
+ const bindingInitType = (name, ancestors) => {
111
+ for (let i = ancestors.length - 1; i >= 0; i -= 1) {
112
+ const a = ancestors[i];
113
+ if (
114
+ a.type === "VariableDeclarator" &&
115
+ a.id?.type === "Identifier" &&
116
+ a.id.name === name
117
+ ) {
118
+ return a.init?.type ?? "none";
119
+ }
120
+ if (a.type === "FunctionDeclaration" && a.id?.name === name) {
121
+ return "FunctionDeclaration";
122
+ }
123
+ const stmts = blockBody(a);
124
+ if (stmts) {
125
+ for (const stmt of stmts) {
126
+ if (stmt.type !== "VariableDeclaration") {
127
+ continue;
128
+ }
129
+ for (const decl of stmt.declarations) {
130
+ if (decl.id?.type === "Identifier" && decl.id.name === name) {
131
+ return decl.init?.type ?? "none";
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ return null;
138
+ };
139
+
140
+ const isFunctionBinding = (name, ancestors, moduleFns) => {
141
+ const local = bindingInitType(name, ancestors);
142
+ if (local === "FunctionDeclaration") {
143
+ return true;
144
+ }
145
+ if (local !== null) {
146
+ return FN_TYPES.has(local);
147
+ }
148
+ return moduleFns.has(name);
149
+ };
150
+
151
+ const enclosingFunctions = (ancestors) =>
152
+ ancestors.filter((a) => FN_TYPES.has(a.type));
153
+
154
+ // A call only runs conditionally when it sits in a branch, not in the test or
155
+ // in the left operand that always evaluates.
156
+ const isGuardedBy = (ancestor, child) => {
157
+ switch (ancestor.type) {
158
+ case "IfStatement":
159
+ case "ConditionalExpression": {
160
+ return child !== ancestor.test;
161
+ }
162
+ case "LogicalExpression": {
163
+ return child === ancestor.right;
164
+ }
165
+ case "SwitchStatement": {
166
+ return child !== ancestor.discriminant;
167
+ }
168
+ case "ForStatement": {
169
+ return child !== ancestor.init;
170
+ }
171
+ case "ForInStatement":
172
+ case "ForOfStatement": {
173
+ return child !== ancestor.right;
174
+ }
175
+ default: {
176
+ return true;
177
+ }
178
+ }
179
+ };
180
+
181
+ const isNestedCallback = (ancestors, index) => {
182
+ const fn = ancestors[index];
183
+ const parent = ancestors[index - 1];
184
+ if (!parent || fn.generator === true || !CALLBACK_PARENTS.has(parent.type)) {
185
+ return false;
186
+ }
187
+ if (parent.type === "CallExpression" && parent.callee === fn) {
188
+ return false;
189
+ }
190
+ return ancestors.slice(0, index).some((a) => FN_TYPES.has(a.type));
191
+ };
192
+
193
+ const noConditionalPrimitive = {
194
+ create(context) {
195
+ const b = bindings();
196
+ const fnStack = [];
197
+ const enterFn = (node) => {
198
+ fnStack.push({ awaited: false, node });
199
+ };
200
+ const exitFn = () => {
201
+ fnStack.pop();
202
+ };
203
+ const markAwait = () => {
204
+ const top = fnStack.at(-1);
205
+ if (top) {
206
+ top.awaited = true;
207
+ }
208
+ };
209
+ return {
210
+ ArrowFunctionExpression: enterFn,
211
+ "ArrowFunctionExpression:exit": exitFn,
212
+ AwaitExpression: markAwait,
213
+ CallExpression(node) {
214
+ const name = primitiveName(node, b);
215
+ if (!name) {
216
+ return;
217
+ }
218
+ const ancestors = ancestorsOf(context, node);
219
+ for (let i = ancestors.length - 1; i >= 0; i -= 1) {
220
+ const a = ancestors[i];
221
+ if (FN_TYPES.has(a.type)) {
222
+ if (isNestedCallback(ancestors, i)) {
223
+ context.report({ data: { name }, messageId: "nested", node });
224
+ } else if (a.async && fnStack.at(-1)?.awaited) {
225
+ context.report({ data: { name }, messageId: "afterAwait", node });
226
+ }
227
+ return;
228
+ }
229
+ const child = ancestors[i + 1] ?? node;
230
+ if (CONDITIONAL.has(a.type) && isGuardedBy(a, child)) {
231
+ context.report({ data: { name }, messageId: "conditional", node });
232
+ return;
233
+ }
234
+ }
235
+ },
236
+ ForOfStatement(node) {
237
+ if (node.await) {
238
+ markAwait();
239
+ }
240
+ },
241
+ FunctionDeclaration: enterFn,
242
+ "FunctionDeclaration:exit": exitFn,
243
+ FunctionExpression: enterFn,
244
+ "FunctionExpression:exit": exitFn,
245
+ ImportDeclaration(node) {
246
+ takeImport(b, node);
247
+ },
248
+ };
249
+ },
250
+ meta: {
251
+ docs: {
252
+ description:
253
+ "Disallow atom/watch/when/context inside conditionals, loops, nested callbacks, or after await",
254
+ url: `${DOCS}#no-conditional-primitive`,
255
+ },
256
+ messages: {
257
+ afterAwait: `Do not call {{name}}() after an await. The component resumes at a different point on rerender, so the primitive order changes. Fix: declare every primitive before the first await. ${see("no-conditional-primitive")}`,
258
+ conditional: `Do not call {{name}}() inside a condition or loop. Primitive registration order must be stable. Fix: call it unconditionally at the top of the component and put the branch inside the primitive or the view. ${see("no-conditional-primitive")}`,
259
+ nested: `Do not call {{name}}() inside a nested callback. Primitives claim slots in call order on the component's own fiber. Fix: move the call to the top level of the component and use its handle in the callback. ${see("no-conditional-primitive")}`,
260
+ },
261
+ schema: [],
262
+ type: "problem",
263
+ },
264
+ };
265
+
266
+ const noPrimitiveOutsideComponent = {
267
+ create(context) {
268
+ const b = bindings();
269
+ return {
270
+ CallExpression(node) {
271
+ const name = primitiveName(node, b);
272
+ if (!name) {
273
+ return;
274
+ }
275
+ const ancestors = ancestorsOf(context, node);
276
+ if (enclosingFunctions(ancestors).length === 0) {
277
+ context.report({ data: { name }, messageId: "outside", node });
278
+ }
279
+ },
280
+ ImportDeclaration(node) {
281
+ takeImport(b, node);
282
+ },
283
+ };
284
+ },
285
+ meta: {
286
+ docs: {
287
+ description:
288
+ "Call atom/watch/when/context inside a component, never at module top level",
289
+ url: `${DOCS}#no-primitive-outside-component`,
290
+ },
291
+ messages: {
292
+ outside: `Call {{name}}() inside a component. There is no fiber at module scope, so this throws at runtime. Fix: move the call into the component function. ${see("no-primitive-outside-component")}`,
293
+ },
294
+ schema: [],
295
+ type: "problem",
296
+ },
297
+ };
298
+
299
+ const noInstructionOutsideGenerator = {
300
+ create(context) {
301
+ const b = bindings();
302
+ return {
303
+ CallExpression(node) {
304
+ const name = primitiveName(node, b);
305
+ if (!name || !INSTRUCTIONS.has(name)) {
306
+ return;
307
+ }
308
+ // yield* wraps the CallExpression in a YieldExpression.
309
+ const parent = ancestorsOf(context, node).at(-1);
310
+ if (parent?.type === "YieldExpression") {
311
+ return;
312
+ }
313
+ const ancestors = ancestorsOf(context, node);
314
+ const fns = enclosingFunctions(ancestors);
315
+ const gen = fns.findLast((f) => f.generator === true);
316
+ if (!gen) {
317
+ context.report({ data: { name }, messageId: "notGenerator", node });
318
+ return;
319
+ }
320
+ // Inside a generator body but not under a yield — the nearest function
321
+ // between the call and the generator must not be a plain callback that
322
+ // escapes the frame. If the call sits directly in the generator, require yield*.
323
+ const genIdx = ancestors.indexOf(gen);
324
+ const between = ancestors
325
+ .slice(genIdx + 1)
326
+ .filter((a) => FN_TYPES.has(a.type));
327
+ if (between.length === 0) {
328
+ context.report({ data: { name }, messageId: "notYielded", node });
329
+ }
330
+ },
331
+ ImportDeclaration(node) {
332
+ takeImport(b, node);
333
+ },
334
+ };
335
+ },
336
+ meta: {
337
+ docs: {
338
+ description: "Use when only inside a generator, with yield*",
339
+ url: `${DOCS}#no-instruction-outside-generator`,
340
+ },
341
+ messages: {
342
+ notGenerator: `{{name}}() returns an instruction that only a generator frame can run. Fix: make the enclosing component a generator (function*) and yield* it. ${see("no-instruction-outside-generator")}`,
343
+ notYielded: `Call yield* with {{name}}() — calling it directly discards the instruction. Fix: write yield* {{name}}(...). ${see("no-instruction-outside-generator")}`,
344
+ },
345
+ schema: [],
346
+ type: "problem",
347
+ },
348
+ };
349
+
350
+ const isIntrinsicElement = (opening) =>
351
+ opening.name.type === "JSXIdentifier" && /^[a-z]/u.test(opening.name.name);
352
+
353
+ const reactEventName = (name) =>
354
+ REACT_EVENT_ALIASES.get(name) ?? `on${name.slice(2).toLowerCase()}`;
355
+
356
+ const isReactEventName = (name) => /^on[A-Z]/u.test(name);
357
+
358
+ const preferLowercaseEvents = {
359
+ create(context) {
360
+ return {
361
+ JSXOpeningElement(opening) {
362
+ if (!isIntrinsicElement(opening)) {
363
+ return;
364
+ }
365
+ for (const attr of opening.attributes) {
366
+ if (
367
+ attr.type !== "JSXAttribute" ||
368
+ attr.name.type !== "JSXIdentifier" ||
369
+ !isReactEventName(attr.name.name)
370
+ ) {
371
+ continue;
372
+ }
373
+ const { name } = attr.name;
374
+ const suggested = reactEventName(name);
375
+ context.report({
376
+ data: { name, suggested },
377
+ fix: (fixer) => fixer.replaceText(attr.name, suggested),
378
+ messageId: "camel",
379
+ node: attr.name,
380
+ });
381
+ }
382
+ },
383
+ };
384
+ },
385
+ meta: {
386
+ docs: {
387
+ description: "Use lowercase DOM event props (onclick, not onClick)",
388
+ url: `${DOCS}#prefer-lowercase-events`,
389
+ },
390
+ fixable: "code",
391
+ messages: {
392
+ camel: `Use lowercase event "{{suggested}}" instead of "{{name}}" — Ilha binds DOM event names, not React ones. Fix: rename the prop to {{suggested}}. ${see("prefer-lowercase-events")}`,
393
+ },
394
+ schema: [],
395
+ type: "problem",
396
+ },
397
+ };
398
+
399
+ const noReactProps = {
400
+ create(context) {
401
+ return {
402
+ JSXOpeningElement(opening) {
403
+ if (!isIntrinsicElement(opening)) {
404
+ return;
405
+ }
406
+ for (const attr of opening.attributes) {
407
+ if (
408
+ attr.type !== "JSXAttribute" ||
409
+ attr.name.type !== "JSXIdentifier"
410
+ ) {
411
+ continue;
412
+ }
413
+ const { name } = attr.name;
414
+ const suggested = isReactEventName(name)
415
+ ? reactEventName(name)
416
+ : REACT_PROPS.get(name);
417
+ if (!suggested) {
418
+ continue;
419
+ }
420
+ context.report({
421
+ data: { name, suggested },
422
+ fix: (fixer) => fixer.replaceText(attr.name, suggested),
423
+ messageId: "react",
424
+ node: attr.name,
425
+ });
426
+ }
427
+ },
428
+ };
429
+ },
430
+ meta: {
431
+ docs: {
432
+ description:
433
+ "Use DOM attribute names on intrinsic JSX elements (class, for, onclick), not React ones",
434
+ url: `${DOCS}#no-react-props`,
435
+ },
436
+ fixable: "code",
437
+ messages: {
438
+ react: `Ilha renders DOM attribute names, so "{{name}}" does nothing useful on a native element. Fix: use "{{suggested}}". ${see("no-react-props")}`,
439
+ },
440
+ schema: [],
441
+ type: "problem",
442
+ },
443
+ };
444
+
445
+ const jsxName = (opening) => {
446
+ const { name } = opening;
447
+ if (name.type === "JSXIdentifier") {
448
+ return name.name;
449
+ }
450
+ if (name.type === "JSXMemberExpression") {
451
+ return "member";
452
+ }
453
+ return null;
454
+ };
455
+
456
+ const isComponentElement = (node) => {
457
+ if (node?.type !== "JSXElement") {
458
+ return false;
459
+ }
460
+ const name = jsxName(node.openingElement);
461
+ return name !== null && !/^[a-z]/u.test(name);
462
+ };
463
+
464
+ const hasKey = (node) =>
465
+ node.openingElement.attributes.some(
466
+ (attr) =>
467
+ attr.type === "JSXSpreadAttribute" ||
468
+ (attr.type === "JSXAttribute" &&
469
+ attr.name.type === "JSXIdentifier" &&
470
+ attr.name.name === "key")
471
+ );
472
+
473
+ const returnedElements = (fn) => {
474
+ const { body } = fn;
475
+ if (body.type !== "BlockStatement") {
476
+ return [body];
477
+ }
478
+ return body.body
479
+ .filter((stmt) => stmt.type === "ReturnStatement")
480
+ .map((stmt) => stmt.argument);
481
+ };
482
+
483
+ const requireKeyInMap = {
484
+ create(context) {
485
+ return {
486
+ CallExpression(node) {
487
+ const { callee } = node;
488
+ if (
489
+ callee.type !== "MemberExpression" ||
490
+ callee.computed ||
491
+ callee.property.type !== "Identifier" ||
492
+ callee.property.name !== "map"
493
+ ) {
494
+ return;
495
+ }
496
+ const [cb] = node.arguments;
497
+ if (
498
+ !cb ||
499
+ (cb.type !== "ArrowFunctionExpression" &&
500
+ cb.type !== "FunctionExpression")
501
+ ) {
502
+ return;
503
+ }
504
+ for (const el of returnedElements(cb)) {
505
+ if (isComponentElement(el) && !hasKey(el)) {
506
+ context.report({
507
+ data: { name: jsxName(el.openingElement) },
508
+ messageId: "key",
509
+ node: el.openingElement,
510
+ });
511
+ }
512
+ }
513
+ },
514
+ };
515
+ },
516
+ meta: {
517
+ docs: {
518
+ description: "Components returned from .map() need a key prop",
519
+ url: `${DOCS}#require-key-in-map`,
520
+ },
521
+ messages: {
522
+ key: `<{{name}}> returned from .map() has no key. Without a stable key, Ilha reuses component instances by position and their atoms attach to the wrong item. Fix: add key={item.id}. ${see("require-key-in-map")}`,
523
+ },
524
+ schema: [],
525
+ type: "problem",
526
+ },
527
+ };
528
+
529
+ const functionInAtom = {
530
+ create(context) {
531
+ const b = bindings();
532
+ const fns = new Set();
533
+ return {
534
+ CallExpression(node) {
535
+ const [arg] = node.arguments;
536
+ if (!arg || primitiveName(node, b) !== "atom") {
537
+ return;
538
+ }
539
+ if (FN_TYPES.has(arg.type)) {
540
+ context.report({ messageId: "derived", node: arg });
541
+ return;
542
+ }
543
+ if (
544
+ arg.type === "Identifier" &&
545
+ isFunctionBinding(arg.name, ancestorsOf(context, arg), fns)
546
+ ) {
547
+ context.report({
548
+ data: { name: arg.name },
549
+ messageId: "init",
550
+ node: arg,
551
+ });
552
+ }
553
+ },
554
+ FunctionDeclaration(node) {
555
+ if (node.id) {
556
+ fns.add(node.id.name);
557
+ }
558
+ },
559
+ ImportDeclaration(node) {
560
+ takeImport(b, node);
561
+ },
562
+ VariableDeclarator(node) {
563
+ const { init } = node;
564
+ if (node.id.type === "Identifier" && init && FN_TYPES.has(init.type)) {
565
+ fns.add(node.id.name);
566
+ }
567
+ },
568
+ };
569
+ },
570
+ meta: {
571
+ docs: {
572
+ description:
573
+ "atom(fn) and atom(() => …) are invalid — use atom.lazy, Atom.map, or Atom.transform",
574
+ url: `${DOCS}#function-in-atom`,
575
+ },
576
+ messages: {
577
+ derived: `atom(() => …) is not supported. Fix: use atom.lazy(() => …) for one-time init, or atom(Atom.map(...)) / atom(Atom.transform(...)) for derived values. ${see("function-in-atom")}`,
578
+ init: `atom({{name}}) is not valid. Fix: use atom.lazy(() => {{name}}) to store a function value. ${see("function-in-atom")}`,
579
+ },
580
+ schema: [],
581
+ type: "problem",
582
+ },
583
+ };
584
+
585
+ module.exports = {
586
+ meta: { name: "oxlint-plugin-ilha" },
587
+ rules: {
588
+ "function-in-atom": functionInAtom,
589
+ "no-conditional-primitive": noConditionalPrimitive,
590
+ "no-instruction-outside-generator": noInstructionOutsideGenerator,
591
+ "no-primitive-outside-component": noPrimitiveOutsideComponent,
592
+ "no-react-props": noReactProps,
593
+ "prefer-lowercase-events": preferLowercaseEvents,
594
+ "require-key-in-map": requireKeyInMap,
595
+ },
596
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "description": "Generator-first island UI on Effect",
5
5
  "keywords": [
6
6
  "effect",
@@ -22,7 +22,12 @@
22
22
  "url": "git+https://github.com/ilhajs/ilha.git"
23
23
  },
24
24
  "files": [
25
- "dist"
25
+ "dist",
26
+ "AGENTS.md",
27
+ "llms.txt",
28
+ "MIGRATIONS.md",
29
+ "MIGRATIONS.json",
30
+ "oxlint.cjs"
26
31
  ],
27
32
  "type": "module",
28
33
  "sideEffects": false,
@@ -39,30 +44,31 @@
39
44
  },
40
45
  "./jsx-runtime": {
41
46
  "types": "./dist/jsx-runtime.d.ts",
42
- "import": "./dist/jsx-runtime.js",
43
- "default": "./src/jsx-runtime.ts"
47
+ "import": "./dist/jsx-runtime.js"
44
48
  },
45
- "./jsx-runtime.ts": "./src/jsx-runtime.ts",
46
49
  "./jsx-dev-runtime": {
47
50
  "types": "./dist/jsx-dev-runtime.d.ts",
48
- "import": "./dist/jsx-dev-runtime.js",
49
- "default": "./src/jsx-dev-runtime.ts"
50
- },
51
- "./jsx-dev-runtime.ts": "./src/jsx-dev-runtime.ts",
52
- "./define": {
53
- "types": "./dist/define.d.ts",
54
- "import": "./dist/define.js"
51
+ "import": "./dist/jsx-dev-runtime.js"
55
52
  },
56
53
  "./renderer": {
57
54
  "types": "./dist/renderer.d.ts",
58
55
  "import": "./dist/renderer.js"
59
- }
56
+ },
57
+ "./test": {
58
+ "types": "./dist/test.d.ts",
59
+ "import": "./dist/test.js"
60
+ },
61
+ "./debug": {
62
+ "types": "./dist/debug.d.ts",
63
+ "import": "./dist/debug.js"
64
+ },
65
+ "./oxlint": "./oxlint.cjs"
60
66
  },
61
67
  "publishConfig": {
62
68
  "access": "public"
63
69
  },
64
70
  "scripts": {
65
- "build": "tsdown",
71
+ "build": "bun scripts/agent-docs.ts && tsdown",
66
72
  "cleanup": "rimraf dist node_modules",
67
73
  "test": "bun test",
68
74
  "typecheck": "tsc -p tsconfig.types.json --noEmit"
package/dist/define.d.ts DELETED
@@ -1,4 +0,0 @@
1
- import { i as Component } from "./types-DsdtZMhG.js";
2
- //#region src/define.d.ts
3
- export declare const define: (name: string, component: Component) => void;
4
- //#endregion
package/dist/define.js DELETED
@@ -1,18 +0,0 @@
1
- import { t as mount } from "./mount-DKY_jSsG.js";
2
-
3
- //#region src/define.ts
4
- const define = (name, component) => {
5
- customElements.define(name, class extends HTMLElement {
6
- #unmount;
7
- connectedCallback() {
8
- this.#unmount = mount(this, component, { hydrate: this.matches("[data-ilha]") });
9
- }
10
- disconnectedCallback() {
11
- this.#unmount?.();
12
- this.#unmount = void 0;
13
- }
14
- });
15
- };
16
-
17
- //#endregion
18
- export { define };