@b9g/crank 0.7.1 → 0.7.2

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