@b9g/crank 0.7.1 → 0.7.3

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/README.md CHANGED
@@ -1,29 +1,30 @@
1
1
  <div align="center">
2
-
3
2
  <img src="logo.svg" alt="Crank.js Logo" width="200" height="200" />
4
3
 
5
4
  # Crank.js
6
-
7
5
  The Just JavaScript Framework
8
-
9
6
  </div>
10
7
 
11
- ## Try Crank
8
+ ## Get Started
12
9
 
13
- The fastest way to try Crank is via the [online playground](https://crank.js.org/playground). In addition, many of the code examples in these guides feature live previews.
10
+ The fastest way to try Crank is via the [online playground](https://crank.js.org/playground).
14
11
 
15
- ## Why Crank?
16
- **Finally, a framework that feels like JavaScript.**
12
+ Other links:
13
+ - [crank.js.org](https://crank.js.org)
14
+ - [Introducing Crank.js](https://crank.js.org/blog/introducing-crank/)
15
+ - [Examples](https://github.com/bikeshaving/crank/tree/main/examples)
16
+ - [Deep Wiki](https://deepwiki.com/bikeshaving/crank)
17
+
18
+ ## Motivation
19
+ **A framework that feels like JavaScript.**
17
20
 
18
21
  While other frameworks invent new paradigms and force you to learn
19
22
  framework-specific APIs, Crank embraces the language features you already know.
20
- No hooks to memorize, no dependency arrays to debug, no cache invalidation to
21
- manage.
22
23
 
23
- ## Pure JavaScript, No Compromises
24
+ ### Pure JavaScript, No Compromises
24
25
 
25
26
  ```javascript
26
- // Async components just work
27
+ // Async components just work, in the browser and on servers
27
28
  async function UserProfile({userId}) {
28
29
  const user = await fetchUser(userId);
29
30
  return <div>Hello, {user.name}!</div>;
@@ -31,32 +32,33 @@ async function UserProfile({userId}) {
31
32
 
32
33
  // Lifecycle logic with generators feels natural
33
34
  function* Timer() {
35
+ // setup goes here
34
36
  let seconds = 0;
35
37
  const interval = setInterval(() => this.refresh(() => seconds++), 1000);
38
+
36
39
  for ({} of this) {
37
40
  yield <div>Seconds: {seconds}</div>;
38
41
  }
42
+
39
43
  clearInterval(interval); // Cleanup just works
40
44
  }
41
45
  ```
42
46
 
43
- ## Why Developers Choose Crank
47
+ ### Why Developers Choose Crank
44
48
 
45
- - **🎯 Intuitive**: Use `async`/`await` for loading states and `function*` for lifecycle—no new APIs to learn
46
- - **⚡ Fast**: Outperforms React in benchmarks while weighing just 5KB with zero dependencies
47
- - **🔧 Flexible**: Write components in vanilla JavaScript with template literals, or use JSX
48
- - **🧹 Clean**: State lives in function scope, lifecycle code goes where it belongs, no mysterious re-render bugs
49
- - **🌊 Future-proof**: Built on stable JavaScript features, not evolving framework abstractions
49
+ - **Intuitive**: Use `async`/`await` for loading states and `function*` for lifecycles no new APIs to learn
50
+ - **Fast**: Outperforms React in benchmarks while weighing at 13.55KB with zero dependencies
51
+ - **Flexible**: Write components in vanilla JavaScript with template literals, or use JSX
52
+ - **Clean**: State lives in function scope, lifecycle code goes where it belongs, no mysterious re-render bugs
53
+ - **Future-proof**: Built on stable JavaScript features, not evolving framework abstractions
50
54
 
51
- ## The "Just JavaScript" Promise, Delivered
55
+ ### The "Just JavaScript" Promise, Delivered
52
56
 
53
57
  Other frameworks claim to be "just JavaScript" but ask you to think in terms of
54
58
  effects, dependencies, and framework-specific patterns. Crank actually delivers
55
- on that promise—your components are literally just functions that use standard
59
+ on that promise your components are literally just functions that use standard
56
60
  JavaScript control flow.
57
61
 
58
- Ready to write components that feel like the JavaScript you know and love?
59
-
60
62
  ## Installation
61
63
 
62
64
  The Crank package is available on [NPM](https://npmjs.org/@b9g/crank) through
@@ -79,24 +81,6 @@ renderer.render(
79
81
  );
80
82
  ```
81
83
 
82
- ### Importing Crank with the **classic** JSX transform.
83
-
84
- ```jsx live
85
- /** @jsx createElement */
86
- /** @jsxFrag Fragment */
87
- import {createElement, Fragment} from "@b9g/crank";
88
- import {renderer} from "@b9g/crank/dom";
89
-
90
- renderer.render(
91
- <p>This paragraph element is transpiled with the classic transform.</p>,
92
- document.body,
93
- );
94
- ```
95
-
96
- You will likely have to configure your tools to support JSX, especially if you
97
- do not want to use `@jsx` comment pragmas. See below for common tools and
98
- configurations.
99
-
100
84
  ### Importing the JSX template tag.
101
85
 
102
86
  Starting in version `0.5`, the Crank package ships a [tagged template
@@ -115,8 +99,9 @@ renderer.render(jsx`
115
99
 
116
100
  ### ECMAScript Module CDNs
117
101
  Crank is also available on CDNs like [unpkg](https://unpkg.com)
118
- (https://unpkg.com/@b9g/crank?module) and [esm.sh](https://esm.sh)
119
- (https://esm.sh/@b9g/crank) for usage in ESM-ready environments.
102
+ (https://unpkg.com/@b9g/crank?module), [esm.sh](https://esm.sh)
103
+ (https://esm.sh/@b9g/crank), and [esm.run](https://esm.run/@b9g/crank)
104
+ (https://esm.run/@b9g/crank) for usage in ESM-ready environments.
120
105
 
121
106
  ```jsx live
122
107
  /** @jsx createElement */
@@ -194,7 +179,7 @@ import {renderer} from "@b9g/crank/dom";
194
179
  async function LoadingIndicator() {
195
180
  await new Promise(resolve => setTimeout(resolve, 1000));
196
181
  return (
197
- <div style="padding: 20px; text-align: center; background: #f8f9fa; border: 2px dashed #6c757d; border-radius: 8px; color: #6c757d;">
182
+ <div>
198
183
  🐕 Fetching a good boy...
199
184
  </div>
200
185
  );
@@ -208,15 +193,14 @@ async function RandomDog({throttle = false}) {
208
193
  }
209
194
 
210
195
  return (
211
- <div style="text-align: center; padding: 10px; background: #fff; border: 1px solid #dee2e6; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
196
+ <div>
212
197
  <a href={data.message} target="_blank" style="text-decoration: none; color: inherit;">
213
198
  <img
214
199
  src={data.message}
215
200
  alt="A Random Dog"
216
201
  width="300"
217
- style="border-radius: 8px; display: block; margin: 0 auto;"
218
202
  />
219
- <div style="margin-top: 8px; color: #6c757d; font-size: 14px;">
203
+ <div>
220
204
  Click to view full size
221
205
  </div>
222
206
  </a>
@@ -242,14 +226,14 @@ function *RandomDogApp() {
242
226
 
243
227
  for ({} of this) {
244
228
  yield (
245
- <div style="max-width: 400px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
229
+ <div>
246
230
  <RandomDogLoader throttle={throttle} />
247
- <div style="text-align: center; margin-top: 20px;">
248
- <button style="padding: 12px 24px; font-size: 16px; background: #007bff; color: white; border: none; border-radius: 6px; cursor: pointer;">
231
+ <div>
232
+ <button>
249
233
  Show me another dog!
250
234
  </button>
251
- <div style="margin-top: 10px; font-size: 14px; color: #6c757d;">
252
- {throttle ? "🐌 Slow mode enabled" : "Fast mode"}
235
+ <div>
236
+ {throttle ? "Slow mode" : "Fast mode"}
253
237
  </div>
254
238
  </div>
255
239
  </div>
@@ -278,25 +262,13 @@ Here’s the configuration you will need to set up automatic JSX transpilation.
278
262
  }
279
263
  ```
280
264
 
281
- The classic transform is supported as well.
282
-
283
- ```tsconfig.json
284
- {
285
- "compilerOptions": {
286
- "jsx": "react",
287
- "jsxFactory": "createElement",
288
- "jsxFragmentFactory": "Fragment"
289
- }
290
- }
291
- ```
292
-
293
- Crank is written in TypeScript. Refer to [the guide on
265
+ Crank is written in TypeScript and comes with types. Refer to [the guide on
294
266
  TypeScript](https://crank.js.org/guides/working-with-typescript) for more
295
267
  information about Crank types.
296
268
 
297
269
  ```tsx
298
270
  import type {Context} from "@b9g/crank";
299
- function *Timer(this: Context) {
271
+ function *Timer(this: Context<typeof Timer>) {
300
272
  let seconds = 0;
301
273
  const interval = setInterval(() => this.refresh(() => seconds++), 1000);
302
274
  for ({} of this) {
@@ -332,26 +304,6 @@ Automatic transform:
332
304
  }
333
305
  ```
334
306
 
335
- Classic transform:
336
- ```.babelrc.json
337
- {
338
- "plugins": [
339
- "@babel/plugin-syntax-jsx",
340
- [
341
- "@babel/plugin-transform-react-jsx",
342
- {
343
- "runtime": "class",
344
- "pragma": "createElement",
345
- "pragmaFrag": "''",
346
-
347
- "throwIfNamespace": false,
348
- "useSpread": true
349
- }
350
- ]
351
- ]
352
- }
353
- ```
354
-
355
307
  ### [ESLint](https://eslint.org)
356
308
 
357
309
  ESLint is a popular open-source tool for analyzing and detecting problems in JavaScript code.
@@ -391,3 +343,507 @@ export default defineConfig({
391
343
  integrations: [crank()],
392
344
  });
393
345
  ```
346
+
347
+ ## API Reference
348
+
349
+ ### Core Exports
350
+
351
+ ```javascript
352
+ import {
353
+ createElement,
354
+ Fragment,
355
+ Copy,
356
+ Portal,
357
+ Raw,
358
+ Text,
359
+ Context
360
+ } from "@b9g/crank";
361
+
362
+ import {renderer} from "@b9g/crank/dom"; // Browser DOM
363
+ import {renderer} from "@b9g/crank/html"; // Server-side HTML
364
+
365
+ import {jsx, html} from "@b9g/crank/standalone"; // Template tag (no build)
366
+
367
+ import {Suspense, SuspenseList, lazy} from "@b9g/crank/async";
368
+ ```
369
+
370
+ ---
371
+
372
+ ### Component Types
373
+
374
+ **Function Component** - Stateless
375
+ ```javascript
376
+ function Greeting({name = "World"}) {
377
+ return <div>Hello {name}</div>;
378
+ }
379
+ ```
380
+
381
+ **Generator Component** - Stateful with `function*`
382
+ ```javascript
383
+ function* Counter() {
384
+ let count = 0;
385
+ const onclick = () => this.refresh(() => count++);
386
+
387
+ for ({} of this) {
388
+ yield <button onclick={onclick}>Count: {count}</button>;
389
+ }
390
+ }
391
+ ```
392
+
393
+ **Async Component** - Uses `async` for promises
394
+ ```javascript
395
+ async function UserProfile({userId}) {
396
+ const user = await fetch(`/api/users/${userId}`).then(r => r.json());
397
+ return <div>Hello, {user.name}!</div>;
398
+ }
399
+ ```
400
+
401
+ **Async Generator Component** - Stateful + async
402
+ ```javascript
403
+ async function* DataLoader({url}) {
404
+ for ({url} of this) {
405
+ const data = await fetch(url).then(r => r.json());
406
+ yield <div>{data.message}</div>;
407
+ }
408
+ }
409
+ ```
410
+
411
+ ---
412
+
413
+ ### Context API
414
+
415
+ The context is available as `this` in components (or as 2nd parameter).
416
+
417
+ ```javascript
418
+ function Component(props, ctx) {
419
+ console.log(this === ctx); // true
420
+ return props.children;
421
+ }
422
+ ```
423
+
424
+ #### Properties
425
+
426
+ **`this.props`** - Current props (readonly)
427
+
428
+ **`this.isExecuting`** - Whether the component is currently executing
429
+
430
+ **`this.isUnmounted`** - Whether the component is unmounted
431
+
432
+ #### Methods
433
+
434
+ **`this.refresh(callback?)`** - Trigger re-execution
435
+ ```javascript
436
+ this.refresh(); // Simple refresh
437
+ this.refresh(() => count++); // With state update (v0.7+)
438
+ ```
439
+
440
+ **`this.schedule(callback?)`** - Execute after DOM is rendered
441
+ ```javascript
442
+ // el is whatever the component returns Node/Text/HTMLElement/null, an array of dom nodes, etc
443
+ this.schedule((el) => {
444
+ console.log("Component rendered". el.innerHTML);
445
+ });
446
+ ```
447
+
448
+ **`this.after(callback?)`** - Execute after DOM is live
449
+ ```javascript
450
+ // this runs after the DOM nodes have finally entered the DOM
451
+ // this is where you put things like autofocus
452
+ this.after((el) => {
453
+ console.log(el.isConnected); // true
454
+ });
455
+ ```
456
+
457
+ **`this.cleanup(callback?)`** - Execute on unmount
458
+ ```javascript
459
+ function* Component() {
460
+ const interval = setInterval(() => this.refresh(), 1000);
461
+ this.cleanup(() => clearInterval(interval));
462
+
463
+ for ({} of this) {
464
+ yield <div>Tick</div>;
465
+ }
466
+ }
467
+ ```
468
+
469
+ **`this.addEventListener(type, listener, options?)`** - Listen to events
470
+ ```javascript
471
+ this.addEventListener("click", (e) => console.log("Clicked!"));
472
+ ```
473
+
474
+ **`this.dispatchEvent(event)`** - Dispatch events
475
+ ```javascript
476
+ this.dispatchEvent(new CustomEvent("mybuttonclick", {
477
+ bubbles: true,
478
+ detail: {id: props.id}
479
+ }));
480
+ ```
481
+
482
+ **`this.provide(key, value)`** / **`this.consume(key)`** - Context API
483
+ ```javascript
484
+ // Provider
485
+ function* ThemeProvider() {
486
+ this.provide("theme", "dark");
487
+ for ({} of this) {
488
+ yield this.props.children;
489
+ }
490
+ }
491
+
492
+ // Consumer
493
+ function ThemedButton() {
494
+ const theme = this.consume("theme");
495
+ return <button class={theme}>Click me</button>;
496
+ }
497
+ ```
498
+
499
+ #### Iteration
500
+
501
+ **`for ({} of this)`** - Render loop (sync)
502
+ ```javascript
503
+ function* Component() {
504
+ for ({} of this) {
505
+ yield <div>{this.props.message}</div>;
506
+ }
507
+ }
508
+ ```
509
+
510
+ **`for await ({} of this)`** - Async render loop for racing trees
511
+ ```javascript
512
+ async function* AsyncComponent() {
513
+ for await ({} of this) {
514
+ // Multiple yields race - whichever completes first shows
515
+ yield <Loading />;
516
+ yield <Content />;
517
+ }
518
+ }
519
+ ```
520
+
521
+ ---
522
+
523
+ ### Special Props
524
+
525
+ **`key`** - Unique identifier for reconciliation
526
+ ```javascript
527
+ <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>
528
+ ```
529
+
530
+ **`ref`** - Access rendered DOM element
531
+ ```javascript
532
+ <audio ref={(el) => (audio = el)} />
533
+
534
+ // Forward refs through components
535
+ function MyInput({ref, ...props}) {
536
+ return <input ref={ref} {...props} />;
537
+ }
538
+ ```
539
+
540
+ **`copy`** - Prevent/control re-rendering
541
+ ```javascript
542
+ // Boolean: prevent rendering when truthy
543
+ <li copy={!el.hasChanged}>{el.value}</li>
544
+
545
+ // string: copy specific props
546
+ <input copy="!value" type="text" /> // Copy all except value
547
+ <div copy="class id" /> // Copy only class and id
548
+ <div copy="children" /> // Copy children
549
+ ```
550
+
551
+ **`hydrate`** - Control SSR hydration
552
+ ```javascript
553
+ <div hydrate={false}> // Skip hydration
554
+ <Portal hydrate={true}> // Force hydration
555
+ <input hydrate="!value" /> // Hydrate all except value
556
+ ```
557
+
558
+ **`class`** - String or object (v0.7+)
559
+ ```javascript
560
+ <button class="btn active" />
561
+
562
+ <button class={{
563
+ btn: true,
564
+ 'btn-active': isActive,
565
+ 'btn-disabled': isDisabled
566
+ }} />
567
+ ```
568
+
569
+ **`style`** - CSS string or object
570
+ ```javascript
571
+ <div style="color: red; font-size: 16px" />
572
+ <div style={{"font-size": "16px", color: "blue"}} />
573
+ ```
574
+
575
+ **`innerHTML`** - Raw HTML string (⚠️ XSS risk)
576
+ ```javascript
577
+ <div innerHTML="<strong>Bold</strong>" />
578
+ ```
579
+
580
+ **Event Props** - Lowercase event handlers
581
+ ```javascript
582
+ <button onclick={handler} />
583
+ <input onchange={handler} oninput={handler} />
584
+ <form onsubmit={handler} />
585
+ ```
586
+
587
+ **Prop Naming** - HTML-friendly names supported
588
+ ```javascript
589
+ <label class="my-label" for="my-id">Label</label>
590
+ // Instead of className and htmlFor
591
+ ```
592
+
593
+ ---
594
+
595
+ ### Special Element Tags
596
+
597
+ **`<Fragment>`** - Render children without wrapper
598
+ ```javascript
599
+ import {Fragment} from "@b9g/crank";
600
+
601
+ <Fragment>
602
+ <div>Child 1</div>
603
+ <div>Child 2</div>
604
+ </Fragment>
605
+
606
+ // Or use: <>...</>
607
+ // The Fragment tag is the empty string
608
+ ```
609
+
610
+ **`<Copy />`** - Prevent element re-rendering
611
+ ```javascript
612
+ import {Copy} from "@b9g/crank";
613
+
614
+ function memo(Component) {
615
+ return function* Wrapped(props) {
616
+ yield <Component {...props} />;
617
+ for (const newProps of this) {
618
+ if (equals(props, newProps)) {
619
+ yield <Copy />; // Reuse previous render
620
+ } else {
621
+ yield <Component {...newProps} />;
622
+ }
623
+ props = newProps;
624
+ }
625
+ };
626
+ }
627
+ ```
628
+
629
+ **`<Portal>`** - Render into different DOM node
630
+ ```javascript
631
+ import {Portal} from "@b9g/crank";
632
+
633
+ const modalRoot = document.getElementById("modal-root");
634
+
635
+ function Modal({children}) {
636
+ return (
637
+ <Portal root={modalRoot}>
638
+ <div class="modal">
639
+ {children}
640
+ </div>
641
+ </Portal>
642
+ );
643
+ }
644
+ ```
645
+
646
+ **`<Raw>`** - Insert raw HTML or DOM nodes
647
+ ```javascript
648
+ import {Raw} from "@b9g/crank";
649
+
650
+ function MarkdownViewer({markdown}) {
651
+ const html = marked(markdown);
652
+ return <div><Raw value={html} /></div>;
653
+ }
654
+
655
+ // Or insert DOM node
656
+ <Raw value={domNode} />
657
+ ```
658
+
659
+ **`<Text>`** - Explicit text node creation (v0.7+)
660
+ ```javascript
661
+ import {Text} from "@b9g/crank";
662
+
663
+ <Text value="Hello world" />
664
+
665
+ // Access Text nodes in lifecycle
666
+ function* Component() {
667
+ this.schedule((node) => {
668
+ if (node instanceof Text) {
669
+ console.log("Text node:", node);
670
+ }
671
+ });
672
+ for ({} of this) {
673
+ yield "Text content"; // Becomes a Text node
674
+ }
675
+ }
676
+ ```
677
+
678
+ ---
679
+
680
+ ### Async Utilities (v0.7+)
681
+
682
+ **`lazy(loader)`** - Lazy-load components
683
+ ```javascript
684
+ import {lazy} from "@b9g/crank/async";
685
+
686
+ const LazyComponent = lazy(() => import("./MyComponent.js"));
687
+
688
+ <Suspense fallback={<div>Loading...</div>}>
689
+ <LazyComponent />
690
+ </Suspense>
691
+ ```
692
+
693
+ **`Suspense`** - Declarative loading states
694
+ ```javascript
695
+ import {Suspense} from "@b9g/crank/async";
696
+
697
+ <Suspense fallback={<div>Loading...</div>}>
698
+ <AsyncComponent />
699
+ </Suspense>
700
+ ```
701
+
702
+ **`SuspenseList`** - Coordinate multiple async components
703
+ ```javascript
704
+ import {SuspenseList} from "@b9g/crank/async";
705
+
706
+ <SuspenseList>
707
+ <Suspense fallback={<div>Loading 1...</div>}>
708
+ <Item1 />
709
+ </Suspense>
710
+ <Suspense fallback={<div>Loading 2...</div>}>
711
+ <Item2 />
712
+ </Suspense>
713
+ </SuspenseList>
714
+ ```
715
+
716
+ ---
717
+
718
+ ### Lifecycle Patterns
719
+
720
+ **Mount** - Code before first `yield`
721
+ ```javascript
722
+ function* Component() {
723
+ console.log("Mounting...");
724
+ const interval = setInterval(() => this.refresh(), 1000);
725
+
726
+ for ({} of this) {
727
+ yield <div>Tick</div>;
728
+ }
729
+
730
+ clearInterval(interval); // Cleanup
731
+ }
732
+ ```
733
+
734
+ **Update** - Code inside render loop
735
+ ```javascript
736
+ function* Component() {
737
+ for ({} of this) {
738
+ console.log("Updated with:", this.props);
739
+ yield <div>{this.props.message}</div>;
740
+ }
741
+ }
742
+ ```
743
+
744
+ **Cleanup** - Code after loop or via `this.cleanup()`
745
+ ```javascript
746
+ function* Component() {
747
+ const interval = setInterval(() => this.refresh(), 1000);
748
+ this.cleanup(() => clearInterval(interval));
749
+
750
+ for ({} of this) {
751
+ yield <div>Tick</div>;
752
+ }
753
+ }
754
+ ```
755
+
756
+ ---
757
+
758
+ ### Advanced Patterns
759
+
760
+ **Higher-Order Components**
761
+ ```javascript
762
+ function withLogger(Component) {
763
+ return function* WrappedComponent(props) {
764
+ console.log("Rendering with:", props);
765
+ for ({} of this) {
766
+ yield <Component {...props} />;
767
+ }
768
+ };
769
+ }
770
+ ```
771
+
772
+ **Hooks**
773
+ ```javascript
774
+ function useInterval(ctx, callback, delay) {
775
+ let interval = setInterval(callback, delay);
776
+ ctx.cleanup(() => clearInterval(interval);
777
+ return (newDelay) => {
778
+ delay = newDelay;
779
+ clearInterval(interval);
780
+ interval = setInterval(callback, delay);
781
+ };
782
+ }
783
+ ```
784
+
785
+ **Context Extensions** (⚠️ Prefer hooks over global extensions)
786
+ ```javascript
787
+ import {Context} from "@b9g/crank";
788
+
789
+ Context.prototype.setInterval = function(callback, delay) {
790
+ const interval = setInterval(callback, delay);
791
+ this.cleanup(() => clearInterval(interval));
792
+ };
793
+
794
+ // Use in components
795
+ function* Timer() {
796
+ let seconds = 0;
797
+ this.setInterval(() => this.refresh(() => seconds++), 1000);
798
+
799
+ for ({} of this) {
800
+ yield <div>Seconds: {seconds}</div>;
801
+ }
802
+ }
803
+ ```
804
+
805
+ **Racing Components**
806
+ ```javascript
807
+ async function* DataComponent({url}) {
808
+ for await ({url} of this) {
809
+ yield <Spinner />;
810
+ yield <Data data={data} />;
811
+ }
812
+ }
813
+ ```
814
+
815
+ ---
816
+
817
+ ### TypeScript Support
818
+
819
+ ```typescript
820
+ import type {Context} from "@b9g/crank";
821
+ import {ComponentProps} from "@b9g/crank"; // v0.7+
822
+
823
+ // Component with typed props
824
+ interface Props {
825
+ name: string;
826
+ age?: number;
827
+ }
828
+
829
+ function Greeting({name, age}: Props) {
830
+ return <div>Hello {name}, age {age}</div>;
831
+ }
832
+
833
+ // Generator with typed context
834
+ function* Greeting(this: Context<typeof Greeting>, {name}: {name: string}) {
835
+ for ({name} of this) {
836
+ yield <div>Hello {name}</div>;
837
+ }
838
+ }
839
+
840
+ // Extract component props type
841
+ function Button({variant}: {variant: "primary" | "secondary"}) {
842
+ return <button class={`btn-${variant}`}>Click</button>;
843
+ }
844
+
845
+ type ButtonProps = ComponentProps<typeof Button>;
846
+ ```
847
+
848
+ For comprehensive guides and documentation, visit [crank.js.org](https://crank.js.org)
849
+