From 7b558ccdbb00efaa12bb0b46a02ee52320533e79 Mon Sep 17 00:00:00 2001 From: Brian Kim Date: Wed, 22 Mar 2023 00:01:28 -0700 Subject: [PATCH] Update documentation --- .eslintrc.json | 18 +- README.md | 422 +++++++++++++++--- package-lock.json | 202 ++------- packages/eslint-plugin-crank/README.md | 17 + packages/eslint-plugin-crank/index.js | 30 ++ packages/eslint-plugin-crank/package.json | 10 + .../blog/2020-04-15-introducing-crank.md | 6 +- .../2020-10-13-writing-crank-from-scratch.md | 16 +- .../documents/guides/01-getting-started.md | 222 ++++++--- website/documents/guides/02-elements.md | 82 +++- website/documents/guides/03-components.md | 178 +++----- .../documents/guides/04-handling-events.md | 233 +++++----- .../documents/guides/05-async-components.md | 140 ++---- .../documents/guides/12-jsx-template-tag.md | 97 ++-- website/documents/index.md | 89 +++- website/examples/minesweeper.ts | 137 ++++++ website/examples/wizard.js | 58 +++ website/package-lock.json | 82 ++-- website/package.json | 16 +- website/src/{serve.ts => app.ts} | 29 +- website/src/build.ts | 84 ---- website/src/clients/playground.ts | 4 +- website/src/components/blog-content.ts | 9 +- website/src/components/code-editor.ts | 7 +- website/src/components/code-preview.ts | 19 +- website/src/components/contentarea.ts | 7 +- website/src/components/esbuild.ts | 30 +- website/src/components/google-spyware.ts | 21 - website/src/components/inline-code-block.ts | 21 +- website/src/components/navbar.ts | 1 + website/src/components/root.ts | 2 - .../src/components/serialize-javascript.ts | 2 +- website/src/single-file-app.ts | 99 ++++ website/src/styles/client.css | 15 +- website/src/views/blog-home.ts | 11 +- website/src/views/blog.ts | 17 +- website/src/views/guide.ts | 12 +- website/src/views/home.ts | 4 +- website/src/views/playground.ts | 6 + 39 files changed, 1523 insertions(+), 932 deletions(-) create mode 100644 packages/eslint-plugin-crank/README.md create mode 100644 packages/eslint-plugin-crank/index.js create mode 100644 packages/eslint-plugin-crank/package.json create mode 100644 website/examples/minesweeper.ts create mode 100644 website/examples/wizard.js rename website/src/{serve.ts => app.ts} (63%) delete mode 100644 website/src/build.ts delete mode 100644 website/src/components/google-spyware.ts create mode 100644 website/src/single-file-app.ts diff --git a/.eslintrc.json b/.eslintrc.json index 342d5745c..3c51598d8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,20 +12,13 @@ }, "plugins": [ "@typescript-eslint", - "prettier", - "react" + "prettier" ], "extends": [ "eslint:recommended", - "plugin:prettier/recommended" + "plugin:prettier/recommended", + "plugin:crank/recommended" ], - - "settings": { - "react": { - "pragma": "createElement", - "fragment": "Fragment" - } - }, "rules": { "no-console": [ "error", @@ -41,7 +34,6 @@ "argsIgnorePattern": "^_" } ], - "no-empty-pattern": 0, "no-dupe-class-members": 0, "@typescript-eslint/no-dupe-class-members": 1, "no-undef": 0, @@ -55,8 +47,6 @@ "bracketSpacing": false } ], - "linebreak-style": ["error", "unix"], - "react/jsx-uses-vars": 1, - "react/jsx-uses-react": 1 + "linebreak-style": ["error", "unix"] } } diff --git a/README.md b/README.md index 60d0f2c2b..51f024209 100644 --- a/README.md +++ b/README.md @@ -1,125 +1,437 @@ # Crank.js -### The Just JavaScript web framework. +The Just JavaScript framework -Crank is a web framework where components can be defined with sync functions, async functions and generator functions. The documentation for Crank.js is available at [crank.js.org](https://crank.js.org). +## What is Crank? -## Get Started +Crank is a JavaScript / TypeScript library for building websites and applications. It is a framework where components are defined with plain old functions, including async and generator functions, which `yield` and `return` JSX. -Crank.js is published on NPM under the `@b9g` organization (short for “b*ikeshavin*g”). +## Why is Crank “Just JavaScript?” -```shell -$ npm i @b9g/crank +Many web frameworks claim to be “just JavaScript.” + +Few have as strong a claim as Crank. + +It starts with the idea that you can write components with *all* of JavaScript’s built-in function syntaxes. + +```jsx live +import {renderer} from "@b9g/crank/dom"; + +function *Timer() { + let seconds = 0; + const interval = setInterval(() => { + seconds++; + this.refresh(); + }, 1000); + + for ({} of this) { + yield

{seconds} second{seconds !== 1 && "s"}

; + } + + clearInterval(interval); +} + +renderer.render(, document.body); + +async function Definition({word}) { + // API courtesy https://dictionaryapi.dev + const res = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`); + const data = await res.json(); + const {phonetic, meanings} = data[0]; + const {partOfSpeech, definitions} = meanings[0]; + const {definition} = definitions[0]; + return <> +

{word} {phonetic}

+

{partOfSpeech}. {definition}

+ {/*
{JSON.stringify(data, null, 4)}
*/} + ; +} + +// TODO: Uncomment me. +//renderer.render(, document.body); ``` -### Key Examples +Crank components work like normal JavaScript, using standard control-flow. Props can be destructured. Promises can be awaited. Updates can be iterated. State can be held in scope. + +The result is a simpler developer experience, where you spend less time writing framework integrations and more time writing vanilla JavaScript. + +## Three reasons to choose Crank -#### A Simple Component +### Reason #1: It’s declarative + +Crank works with JSX. It uses tried-and-tested virtual DOM algorithms. Simple components can be defined with functions which return elements. ```jsx live import {renderer} from "@b9g/crank/dom"; function Greeting({name = "World"}) { + return

Hello {name}.

; +} + +function RandomName() { + const names = ["Alice", "Bob", "Carol", "Dave"]; + const randomName = names[Math.floor(Math.random() * names.length)]; + + // TODO: Uncomment the button. return ( -
Hello {name}
+
+ + {/* + + */} +
); } -renderer.render(, document.body); +renderer.render(, document.body); +``` + +Don’t think JSX is vanilla enough? Crank provides a tagged template function which does roughly the same thing. + +```jsx live +import {jsx} from "@b9g/crank/standalone"; +import {renderer} from "@b9g/crank/dom"; + +function Star({cx, cy, r=50, ir, p=5, fill="red"}) { + cx = parseFloat(cx); + cy = parseFloat(cy); + r == parseFloat(r); + ir = ir == null ? r * 0.4 : parseFloat(ir); + p = parseFloat(p); + const points = []; + const angle = Math.PI / p; + for (let i = 0, a = Math.PI / 2; i < p * 2; i++, a += angle) { + const x = cx + Math.cos(a) * (i % 2 === 0 ? r : ir); + const y = cy - Math.sin(a) * (i % 2 === 0 ? r : ir); + points.push([x, y]); + } + + return jsx` + + `; +} + +function Stars({width, height}) { + return jsx` + + + <${Star} cx="70" cy="70" r="50" fill="red" /> + <${Star} cx="80" cy="80" r="50" fill="orange" /> + <${Star} cx="90" cy="90" r="50" fill="yellow" /> + <${Star} cx="100" cy="100" r="50" fill="green" /> + <${Star} cx="110" cy="110" r="50" fill="dodgerblue" /> + <${Star} cx="120" cy="120" r="50" fill="indigo" /> + <${Star} + cx="130" + cy="130" + r="50" + fill="purple" + p=${6} + /> + + `; +} + +const inspirationalWords = [ + "I believe in you.", + "You are great.", + "Get back to work.", + "We got this.", +]; + +function RandomInspirationalWords() { + return jsx` +

${inspirationalWords[Math.floor(Math.random() * inspirationalWords.length)]}

+ `; +} + +renderer.render(jsx` +
+ <${Stars} width=${200} height=${200} /> + <${RandomInspirationalWords} /> +
+`, document.body); +``` + +### Reason #2: It’s predictable + +Crank uses generator functions to define stateful components. You store state in local variables, and `yield` rather than `return` to keep it around. + +```jsx live +import {renderer} from "@b9g/crank/dom"; + +function Greeting({name = "World"}) { + return

Hello {name}.

; +} + +function *CyclingName() { + const names = ["Alice", "Bob", "Carol", "Dave"]; + let i = 0; + while (true) { + yield ( +
+ + +
+ ) + + i++; + } +} + +renderer.render(, document.body); ``` -#### A Stateful Component +Components rerender based on explicit `refresh()` calls. This level of precision means you can be as messy as you need to be. + +Never memoize a callback ever again. ```jsx live import {renderer} from "@b9g/crank/dom"; function *Timer() { + let interval = null; let seconds = 0; - const interval = setInterval(() => { - seconds++; - this.refresh(); - }, 1000); - try { - while (true) { - yield
Seconds: {seconds}
; + const startInterval = () => { + interval = setInterval(() => { + seconds++; + this.refresh(); + }, 1000); + }; + + const toggleInterval = () => { + if (interval == null) { + startInterval(); + } else { + clearInterval(interval); + interval = null; } - } finally { + + this.refresh(); + }; + + const resetInterval = () => { + seconds = 0; clearInterval(interval); + interval = null; + this.refresh(); + }; + + // The this of a Crank component is an iterable of props. + for ({} of this) { + // Welcome to the render loop. + // Most generator components should use render loops even if they do not + // use props. + // The render loop provides useful behavior like preventing infinite loops + // because of a forgotten yield. + yield ( +
+

{seconds} second{seconds !== 1 && "s"}

+ + {" "} + +
+ ); } + + // You can even put cleanup code after the loop. + clearInterval(interval); } renderer.render(, document.body); ``` -#### An Async Component +### Reason #3: It’s promise-friendly. + +Any component can be made asynchronous with the `async` keyword. This means you can await `fetch()` directly in a component, client or server. ```jsx live import {renderer} from "@b9g/crank/dom"; -async function QuoteOfTheDay() { - const res = await fetch("https://favqs.com/api/qotd"); - const {quote} = await res.json(); +async function Definition({word}) { + // API courtesy https://dictionaryapi.dev + const res = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`); + const data = await res.json(); + const {phonetic, meanings} = data[0]; + const {partOfSpeech, definitions} = meanings[0]; + const {definition} = definitions[0]; return ( -

- “{quote.body}” – {quote.author} -

+
+

{word} {phonetic}

+

{partOfSpeech}. {definition}

+
); } -renderer.render(, document.body); +function *Dictionary() { + let word = ""; + const onsubmit = (ev) => { + ev.preventDefault(); + const formData = new FormData(ev.target); + const word1 = formData.get("word"); + if (word1.trim()) { + word = word1; + this.refresh(); + } + }; + + for ({} of this) { + yield ( + <> +
+
+ {" "} + +
+
+ +
+
+ {word && } + + ); + } +} + +renderer.render(, document.body); ``` -### A Loading Component +Async generator functions let you write components that are both async *and* stateful. Crank uses promises wherever it makes sense, and has a rich async execution model which allows you to do things like racing components to display loading states. ```jsx live -import {Fragment} from "@b9g/crank"; import {renderer} from "@b9g/crank/dom"; -async function LoadingIndicator() { - await new Promise(resolve => setTimeout(resolve, 1000)); - return
Fetching a good boy...
; +function formatNumber(number, type) { + number = number.padEnd(16, "0"); + if (type === "American Express") { + return [number.slice(0, 4), number.slice(4, 10), number.slice(10, 15)].join(" "); + } + + return [ + number.slice(0, 4), + number.slice(4, 8), + number.slice(8, 12), + number.slice(12), + ].join(" "); } -async function RandomDog({throttle = false}) { - const res = await fetch("https://dog.ceo/api/breeds/image/random"); - const data = await res.json(); - if (throttle) { - await new Promise(resolve => setTimeout(resolve, 2000)); +function CreditCard({type, expiration, number, owner}) { + return ( +
+
{formatNumber(number, type)}
+
Exp: {expiration}
+ +
{type}
+
{owner}
+
+ ); +} + +async function *LoadingCreditCard() { + await new Promise((r) => setTimeout(r, 1000)); + let count = 0; + const interval = setInterval(() => { + count++; + this.refresh(); + }, 200); + + this.cleanup(() => clearInterval(interval)); + + for ({} of this) { + yield ( + + ); } +} +async function MockCreditCard({throttle}) { + if (throttle) { + await new Promise((r) => setTimeout(r, 2000)); + } + // Mock credit card data courtesy https://fakerapi.it/en + const res = await fetch("https://fakerapi.it/api/v1/credit_cards?_quantity=1"); + if (res.status === 429) { + return ( + Too many requests. Please use free APIs responsibly. + ); + } + const {data: [card]} = await res.json(); return ( - - A Random Dog - + ); } -async function *RandomDogLoader({throttle}) { +async function *RandomCreditCard({throttle}) { + setTimeout(() => this.refresh()); + yield null; for await ({throttle} of this) { - yield ; - yield ; + yield ; + yield ; } } -function *RandomDogApp() { +function *CreditCardGenerator() { let throttle = false; - this.addEventListener("click", (ev) => { - if (ev.target.tagName === "BUTTON") { - throttle = !throttle; - this.refresh(); - } - }); + const toggleThrottle = () => { + throttle = !throttle; + // TODO: A nicer user behavior would be to not generate a new card + // when toggling the throttle. + this.refresh(); + }; for ({} of this) { yield ( - +
- + + {" "} +
- - + +
); } } -renderer.render(, document.body); +renderer.render(, document.body); ``` diff --git a/package-lock.json b/package-lock.json index e613c6582..16a3ebff0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,6 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -1110,7 +1109,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -1125,7 +1123,6 @@ "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -1134,7 +1131,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -1157,7 +1153,6 @@ "version": "8.56.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -1175,7 +1170,6 @@ "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -1189,7 +1183,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "engines": { "node": ">=12.22" }, @@ -1201,8 +1194,7 @@ "node_modules/@humanwhocodes/object-schema": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", @@ -1952,8 +1944,7 @@ "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@vscode/emmet-helper": { "version": "2.9.2", @@ -1995,7 +1986,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2041,7 +2031,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2137,7 +2126,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -2150,7 +2138,6 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -2188,7 +2175,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -2206,7 +2192,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -2224,7 +2209,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -2237,7 +2221,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", - "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", @@ -2805,7 +2788,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" } @@ -2814,7 +2796,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -2835,8 +2816,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -2925,7 +2905,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3032,7 +3011,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2", "get-intrinsic": "^1.2.1", @@ -3046,7 +3024,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -3354,8 +3331,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -3516,8 +3492,7 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/defaults": { "version": "1.0.4", @@ -3535,7 +3510,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.1", "gopd": "^1.0.1", @@ -3549,7 +3523,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -3606,7 +3579,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -3653,7 +3625,6 @@ "version": "1.22.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "arraybuffer.prototype.slice": "^1.0.2", @@ -3706,7 +3677,6 @@ "version": "1.0.15", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", - "dev": true, "dependencies": { "asynciterator.prototype": "^1.0.0", "call-bind": "^1.0.2", @@ -3734,7 +3704,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.2", "has-tostringtag": "^1.0.0", @@ -3748,7 +3717,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" } @@ -3757,7 +3725,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, "dependencies": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -3832,7 +3799,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -3844,7 +3810,6 @@ "version": "8.56.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", - "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3907,6 +3872,10 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-crank": { + "resolved": "packages/eslint-plugin-crank", + "link": true + }, "node_modules/eslint-plugin-prettier": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", @@ -3941,7 +3910,6 @@ "version": "7.33.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", - "dev": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", @@ -3971,7 +3939,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -3983,7 +3950,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -3992,7 +3958,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -4004,7 +3969,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -4020,7 +3984,6 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -4050,7 +4013,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -4062,7 +4024,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -4074,7 +4035,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -4089,7 +4049,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4185,8 +4144,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.3.0", @@ -4223,14 +4181,12 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fastq": { "version": "1.16.0", @@ -4244,7 +4200,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -4309,7 +4264,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -4322,14 +4276,12 @@ "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -4364,8 +4316,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -4392,7 +4343,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -4410,7 +4360,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4436,7 +4385,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dev": true, "dependencies": { "function-bind": "^1.1.2", "has-proto": "^1.0.1", @@ -4463,7 +4411,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -4485,7 +4432,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4505,7 +4451,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -4517,7 +4462,6 @@ "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -4532,7 +4476,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, "dependencies": { "define-properties": "^1.1.3" }, @@ -4567,7 +4510,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -4583,8 +4525,7 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, "node_modules/gray-matter": { "version": "4.0.3", @@ -4627,7 +4568,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4644,7 +4584,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.2" }, @@ -4656,7 +4595,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4668,7 +4606,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4680,7 +4617,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -4879,7 +4815,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true, "engines": { "node": ">= 4" } @@ -4888,7 +4823,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -4914,7 +4848,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -4935,7 +4868,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -4950,7 +4882,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.2", "hasown": "^2.0.0", @@ -4989,7 +4920,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -5003,7 +4933,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -5018,7 +4947,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, "dependencies": { "has-bigints": "^1.0.1" }, @@ -5041,7 +4969,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -5080,7 +5007,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -5103,7 +5029,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -5150,7 +5075,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -5170,7 +5094,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -5207,7 +5130,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5232,7 +5154,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -5252,7 +5173,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -5267,7 +5187,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -5285,7 +5204,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -5301,7 +5219,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5310,7 +5227,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -5333,7 +5249,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -5348,7 +5263,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -5363,7 +5277,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, "dependencies": { "which-typed-array": "^1.1.11" }, @@ -5389,7 +5302,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5398,7 +5310,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -5410,7 +5321,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -5449,8 +5359,7 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/isexe": { "version": "2.0.0", @@ -5518,7 +5427,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", - "dev": true, "dependencies": { "define-properties": "^1.2.1", "get-intrinsic": "^1.2.1", @@ -5557,20 +5465,17 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { "version": "2.2.3", @@ -5605,7 +5510,6 @@ "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -5638,7 +5542,6 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -5664,7 +5567,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -5757,8 +5659,7 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "node_modules/log-symbols": { "version": "5.1.0", @@ -5800,7 +5701,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -6734,7 +6634,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6794,8 +6693,7 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/network-information-types": { "version": "0.1.1", @@ -6874,7 +6772,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6883,7 +6780,6 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6908,7 +6804,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -6917,7 +6812,6 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.5", "define-properties": "^1.2.1", @@ -6935,7 +6829,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -6949,7 +6842,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -6966,7 +6858,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", - "dev": true, "dependencies": { "define-properties": "^1.2.0", "es-abstract": "^1.22.1" @@ -6979,7 +6870,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -6996,7 +6886,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -7019,7 +6908,6 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -7203,7 +7091,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -7250,7 +7137,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7727,7 +7613,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -7846,7 +7731,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -7867,7 +7751,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -7894,8 +7777,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/readable-stream": { "version": "3.6.2", @@ -7954,7 +7836,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -7974,7 +7855,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -8171,7 +8051,6 @@ "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -8188,7 +8067,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -8284,7 +8162,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -8387,7 +8264,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", - "dev": true, "dependencies": { "call-bind": "^1.0.5", "get-intrinsic": "^1.2.2", @@ -8424,7 +8300,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.5", "get-intrinsic": "^1.2.2", @@ -8483,7 +8358,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", - "dev": true, "dependencies": { "define-data-property": "^1.1.1", "function-bind": "^1.1.2", @@ -8499,7 +8373,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "functions-have-names": "^1.2.3", @@ -8583,7 +8456,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -8775,7 +8647,6 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -8795,7 +8666,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -8812,7 +8682,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -8826,7 +8695,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -8894,7 +8762,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -9004,8 +8871,7 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, "node_modules/to-fast-properties": { "version": "2.0.0", @@ -9186,7 +9052,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -9207,7 +9072,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -9219,7 +9083,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.1", @@ -9233,7 +9096,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -9251,7 +9113,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", - "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -9270,7 +9131,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -9296,7 +9156,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", @@ -9541,7 +9400,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -10276,7 +10134,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -10292,7 +10149,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", - "dev": true, "dependencies": { "function.prototype.name": "^1.1.5", "has-tostringtag": "^1.0.0", @@ -10318,7 +10174,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, "dependencies": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -10355,7 +10210,6 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", - "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.4", @@ -10444,8 +10298,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/y18n": { "version": "5.0.8", @@ -10560,6 +10413,13 @@ "@b9g/crank": "^0.5.1", "astro": "^2.0.0" } + }, + "packages/eslint-plugin-crank": { + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "eslint-plugin-react": "^7.32.2" + } } } } diff --git a/packages/eslint-plugin-crank/README.md b/packages/eslint-plugin-crank/README.md new file mode 100644 index 000000000..7975a0db3 --- /dev/null +++ b/packages/eslint-plugin-crank/README.md @@ -0,0 +1,17 @@ +# eslint-config-crank + +An unopinionated baseline ESLint configuration for [Crank.js](https://crank.js.org). + +## Installation + +```bash +npm i -D eslint eslint-config-crank +``` + +In your eslint configuration: + +```.eslintrc.json +{ + "extends": ["plugin:crank/recommended"] +} +``` diff --git a/packages/eslint-plugin-crank/index.js b/packages/eslint-plugin-crank/index.js new file mode 100644 index 000000000..cdb4e7ba3 --- /dev/null +++ b/packages/eslint-plugin-crank/index.js @@ -0,0 +1,30 @@ +module.exports = { + configs: { + recommended: { + plugins: ["react"], + settings: { + react: { + pragma: "createElement", + fragment: "Fragment", + }, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + rules: { + // A common Crank idiom is to destructure the context iterator values + // with an empty pattern. + // for (const {} of this) {} + // TODO: Write a rule that checks for empty patterns outside of this + // context. + "no-empty-pattern": 0, + "react/jsx-uses-react": 2, + "react/jsx-uses-vars": 2, + "react/jsx-no-undef": 2, + "react/jsx-no-duplicate-props": 2, + }, + }, + }, +}; diff --git a/packages/eslint-plugin-crank/package.json b/packages/eslint-plugin-crank/package.json new file mode 100644 index 000000000..6fb230664 --- /dev/null +++ b/packages/eslint-plugin-crank/package.json @@ -0,0 +1,10 @@ +{ + "name": "eslint-plugin-crank", + "version": "0.1.0", + "description": "ESLint plugin for Crank", + "main": "index.js", + "license": "MIT", + "dependencies": { + "eslint-plugin-react": "^7.32.2" + } +} diff --git a/website/documents/blog/2020-04-15-introducing-crank.md b/website/documents/blog/2020-04-15-introducing-crank.md index 22aaa0f32..5f4f68014 100644 --- a/website/documents/blog/2020-04-15-introducing-crank.md +++ b/website/documents/blog/2020-04-15-introducing-crank.md @@ -1,6 +1,8 @@ --- title: Introducing Crank publishDate: 2020-04-15 +author: Brian Kim +authorURL: https://github.com/brainkim --- After months of development, I’m happy to introduce Crank.js, a new framework for creating JSX-driven components with functions, promises and generators. And I know what you’re thinking: *oh no, not another web framework.* There are already so many of them out there and each carries a non-negligible cost in terms of learning it and building an ecosystem to surround it, so it makes sense that you would reject newcomers if only to avoid the deep sense of exhaustion which has come to be known amongst front-end developers as “JavaScript fatigue.” Therefore, this post is both an introduction to Crank as well as an apology: I’m sorry for creating yet another framework, and I hope that by explaining the circumstances which led me to do so, you will forgive me. @@ -8,7 +10,7 @@ After months of development, I’m happy to introduce Crank.js, a new framework I will be honest. Before embarking on this project, I never considered myself capable of making a “web framework.” I don’t maintain any popular open-source libraries, and most of the early commits to this project had messages like “I can’t even believe I’m actually considering making my own web framework.” Before working on Crank, my framework of choice was React, and I had used it dutifully for almost every project within my control since the `React.createClass` days. And as React evolved, I must admit, I was intrigued and excited with the announcement of each new code-named feature like “Fibers,” “Hooks” and “Suspense.” I sincerely felt that React would continue to be relevant well into the 2020s. -![The first commit messages](../static/commits.png) +![The first commit messages](/static/commits.png) However, over time, I grew increasingly alienated by what I perceived to be the more general direction of React, which was to reframe it as a “UI runtime.” Each new API felt exciting, but I disliked how opaque and error-prone the concrete code written with these APIs seemed. I was unhappy, for instance, with the strangeness and pitfalls of the new Hooks API, and I worried about the constant warnings the React team gave about how code which worked today would break once something called “Concurrent Mode” landed. *I already have a UI runtime*, I began to grumble whenever I read the latest on React, *it’s called JavaScript.* @@ -75,7 +77,7 @@ This is some pseudo-code I sketched out, where the calls to `componentDidWhat()` Furthermore, you could implement something like the `componentDidCatch()` and `componentWillUnmount()` lifecycle methods directly within the async generator, by wrapping the `yield` operator in a `try`/`catch`/`finally` block. And the framework could, upon producing DOM nodes, pass these nodes back into the generator, so you could do direct DOM manipulations without React’s notion of “refs.” All these things which React required separate methods or hooks to accomplish could be done within async generator functions with just the control-flow operators that JavaScript provides, and all within the same scope. -This idea didn’t come all at once, but it dazzled me nonetheless, and for the first time I felt like the task of creating a framework was achievable. I didn’t know all the details behind how to implement the API above, and I still didn’t know how the framework would handle async functions or sync generator functions, but I saw the start and the end, something to motivate me when I got stuck. And the best part was that it felt like “innovation arbitrage,” where, while the React team spent its considerable engineering talent on creating a “UI runtime,” I could just delegate the hard stuff to JavaScript. I didn’t need to flatten call stacks into a “fiber” data structure so that computations could be arbitrarily paused and resumed; rather, I could just let the `await` and `yield` operators do the suspending and resuming for me. And I didn’t need to create a scheduler like the React team is doing; rather, I could use promises and the microtask queue to coordinate asynchrony between components. For all the hard things that the React team was doing, a solution seemed latent within the JavaScript runtime. I just had to apply it. +This idea didn’t come all at once, but it dazzled me nonetheless, and for the first time I felt like the task of creating a framework was achievable. I didn’t know all the details behind how to implement the API above, and I still didn’t know how the framework would handle async functions or sync generator functions, but I saw the start and the end, something to motivate me when I got stuck. And the best part was that it felt like “innovation arbitrage,” where, while the React team spent its considerable engineering talent on creating a “UI runtime,” I could just delegate the hard stuff to JavaScript. I didn’t need to flatten call stacks into a “fiber” data structure so that computations could be arbitrarily paused and resumed; rather, I could just let the `await` and `yield` operators do the suspending and resuming for me. And I didn’t need to create a scheduler like the React team was doing; rather, I could use promises and the microtask queue to coordinate asynchrony between components. For all the hard things that the React team was doing, a solution seemed latent within the JavaScript runtime. I just had to apply it. ## Not Just Another Web Framework Crank is the result of a months-long investigation into the viability of this idea, that JSX-based components could be written not just with sync functions, but also with async functions, and with sync and async generator functions. Much of this time was spent refining the design of the API, figuring out what to do, for instance, when an async component is still pending but rerendered, and how things like event handling should work. As it turns out, the simplicity of directly awaiting promises within your components is unmatched by any API the React team has put out, and sync generator functions turned out to be just as if not more useful than async generator functions. I’m very pleased with the result. I literally started tearing up while implementing TodoMVC in Crank, partly because it was the culmination of months of work, but also because it felt so natural and easy. diff --git a/website/documents/blog/2020-10-13-writing-crank-from-scratch.md b/website/documents/blog/2020-10-13-writing-crank-from-scratch.md index 60369ec96..35f397bdf 100644 --- a/website/documents/blog/2020-10-13-writing-crank-from-scratch.md +++ b/website/documents/blog/2020-10-13-writing-crank-from-scratch.md @@ -1,6 +1,8 @@ --- title: Writing Crank from Scratch publishDate: 2020-10-13 +author: Brian Kim +authorURL: https://github.com/brainkim --- One of my goals when authoring Crank.js was to create a framework which was so simple that any intermediate JavaScript developer could conceivably write it from scratch. What I think makes this uniquely achievable for Crank is that its component model is built on top of JavaScript’s two main control flow abstractions, iterators and promises, allowing developers to write components exclusively with sync and async functions and generator functions. @@ -2008,13 +2010,13 @@ What we want is a way to limit the concurrency of async component elements, so t Before we continue, I’d like to introduce a visual notation for promises which we’ll use for the rest of this essay. This is a promise. -![A Promise](../static/promise.png) +![A Promise](/static/promise.png) These diagrams will get more complicated, I *promise*. But for now, know that the horizontal axis represents time, the left edge represents when the promise was created and the right edge represents when the promise settles. Given this notation, we can represent multiple calls to an async function as follows. -![Multiple Promises](../static/multiple-promises.png) +![Multiple Promises](/static/multiple-promises.png) What we want for async components is a strategy which coalesces these promises so that there is only one pending run of a component element at any point in time. Visually, this would mean that none of these line segments overlap. @@ -2022,7 +2024,7 @@ One possible technique we could use is *hitching,* where we resolve concurrent c This strategy would transform the previous diagram to the following one. -![Hitching](../static/hitching.png) +![Hitching](/static/hitching.png) The promises `B` and `C` resolve to the promise `A`, because they are created while `A` is still pending. We use a dotted line to indicate that these promises don’t actually perform any work, and we use the red trailing edge to indicate that these promises have resolved to some other call. Because `D` starts after `A` finishes, it is its own independent promise. @@ -2078,7 +2080,7 @@ We’ve added two more variables to the wrapper function’s scope, `enqueued` a This strategy can be expressed as the following promise diagram. -![Enqueuing](../static/enqueuing.png) +![Enqueuing](/static/enqueuing.png) In this diagram, because `B` and `C` are created while `A` is pending, we enqueue another run. However, only `C` actually does work, because by the time `A` finishes, we only re-invoke the function with `C`’s arguments, while `B`’s arguments would have been overwritten. This is a useful behavior for async components, because we don’t really care about obsolete props or element trees. Lastly, `D` starts while `C` is pending, so we schedule another run for `D`. Note that the original function is not invoked until the current run settles, so we again have a situation where there is only one concurrent run of the original function at a time. @@ -2310,7 +2312,7 @@ To implement this behavior, we’ll need to update the `stepCtx()` function so t We keep the inflight/enqueued pattern from the previous step, except now we advance the queue based on the blocking portion of the render. A promise diagram for this algorithm might look like this. -![Partial Enqueuing](../static/partial-enqueuing.png) +![Partial Enqueuing](/static/partial-enqueuing.png) The blue segments represent the duration for which the component is blocked, while the blue + black segments represent the duration for the entire render. As you can see, this allows for greater concurrency with regard to rendering, while still limiting the number of concurrent runs for each individual async component element to one. @@ -2336,7 +2338,7 @@ console.log(app.innerHTML); We want rendering to ignore outdated renders just as async component enqueuing ignores outdated props and children. To achieve this, we use another promise technique called *chasing.* Chasing involves racing the current call of an async function with the next call, for every call. Visually, we can represent chasing like so. -![Chasing](../static/chasing.png) +![Chasing](/static/chasing.png) In the diagram, the `B` promise takes longer to settle than the `C` promise, so the `B` promise is cut off and made to resolve to the `C` promise. We can represent this algorithm as the following higher-order function. @@ -2361,7 +2363,7 @@ The part of this algorithm that will probably hurt your brain is that we don’t The cool part about this strategy is that it chains, so that when any call settles, we know for a fact that *all* previous calls have settled as well. You can prove this mathematically with a [proof by induction](https://en.wikipedia.org/wiki/Mathematical_induction), or visually with a promise diagram. -![Chained Chasing](../static/chained-chasing.png) +![Chained Chasing](/static/chained-chasing.png) Proving that this algorithm works to settle all previous promises is as simple as drawing a vertical line upwards from the end of any promise in any promise diagram. diff --git a/website/documents/guides/01-getting-started.md b/website/documents/guides/01-getting-started.md index 558b1fa21..7204e4713 100644 --- a/website/documents/guides/01-getting-started.md +++ b/website/documents/guides/01-getting-started.md @@ -2,97 +2,98 @@ title: Getting Started --- + + ## Try Crank -The fastest way to try Crank is via the [playground](/playground). Additionally, many of the code examples in these guides are editable and runnable. + +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. ## Installation + +The Crank package is available on [NPM](https://npmjs.org/@b9g/crank) through +the [@b9g organization](https://www.npmjs.com/org/b9g) (short for +b*ikeshavin*g). + ```shell -$ npm install @b9g/crank +npm i @b9g/crank ``` -The Crank package is available on [NPM](https://npmjs.org/@b9g/crank) through the [@b9g organization](https://www.npmjs.com/org/b9g) (short for b*ikeshavin*g). +### Importing Crank with the **classic** JSX transform. -```jsx +```jsx live /** @jsx createElement */ -import {createElement} from "@b9g/crank"; +/** @jsxFrag Fragment */ +import {createElement, Fragment} from "@b9g/crank"; import {renderer} from "@b9g/crank/dom"; -renderer.render(
Hello world
, document.body); + +renderer.render( +

This paragraph element is transpiled with the classic transform.

, + document.body, +); ``` -It is also available on CDNs like [unpkg](https://unpkg.com) (https://unpkg.com/@b9g/crank?module) and [esm.sh](https://esm.sh) (https://esm.sh/@b9g/crank) for usage in ESM-ready environments. +### Importing Crank with the **automatic** JSX transform. ```jsx live -/** @jsx createElement */ - -// This is an ESM-ready environment! -import {createElement} from "https://unpkg.com/@b9g/crank/crank?module"; -import {renderer} from "https://unpkg.com/@b9g/crank/dom?module"; +/** @jsxImportSource @b9g/crank */ +import {renderer} from "@b9g/crank/dom"; -renderer.render(
Hello world
, document.body); +renderer.render( +

This paragraph element is transpiled with the automatic transform.

, + document.body, +); ``` -## Transpiling JSX -Crank works with [JSX](https://facebook.github.io/jsx/), a well-supported, XML-like syntax extension to JavaScript. The hardest part about setting up a Crank project will probably be configuring your favorite web tools to transpile JSX in a way Crank understands; luckily, this section will walk you through the latest in JSX transforms and configurations. - -### Two types of JSX transpilation -Historically speaking, there are two ways to transform JSX: the *classic* and *automatic* transforms. Crank supports both formats. +You will likely have to configure your tools to support JSX, especially if you do not want to use `@jsx` comment pragmas. See below for common tools and configurations. -The classic transform turns JSX elements into `createElement()` calls. +### Importing the JSX template tag. -```jsx -/** @jsx createElement */ -import {createElement} from "@b9g/crank"; +Starting in version `0.5`, the Crank package ships a [tagged template function](/guides/jsx-template-tag) which provides similar syntax and semantics as the JSX transform. This allows you to write Crank components in vanilla JavaScript. -const el =
An element
; -// Transpiles to: +```js live +import {jsx} from "@b9g/crank/standalone"; +import {renderer} from "@b9g/crank/dom"; -const el = createElement("div", {id: "element"}, "An element"); -// Identifiers like `createElement`, `Fragment` must be manually imported. +renderer.render(jsx` +

No transpilation is necessary with the JSX template tag.

+`, document.body); ``` -The automatic transform turns JSX elements into function calls from an automatically imported namespace. +### ECMAScript Module CDNs +Crank is also available on CDNs like [unpkg](https://unpkg.com) +(https://unpkg.com/@b9g/crank?module) and [esm.sh](https://esm.sh) +(https://esm.sh/@b9g/crank) for usage in ESM-ready environments. -```jsx -/** @jsxImportSource @b9g/crank */ +```jsx live +/** @jsx createElement */ -const profile = ( -
- -

{[user.firstName, user.lastName].join(" ")}

-
-); +// This is an ESM-ready environment! +// If code previews work, your browser is an ESM-ready environment! -// Transpiles to: -import { jsx as _jsx } from "@b9g/crank/jsx-runtime"; -import { jsxs as _jsxs } from "@b9g/crank/jsx-runtime"; - -const profile = _jsxs("div", { - children: [ - _jsx("img", { - src: "avatar.png", - "class": "profile", - }), - _jsx("h3", { - children: [user.firstName, user.lastName].join(" "), - }), - ], -}); +import {createElement} from "https://unpkg.com/@b9g/crank/crank?module"; +import {renderer} from "https://unpkg.com/@b9g/crank/dom?module"; +renderer.render( +
+ Running on unpkg.com +
, + document.body, +); ``` -The automatic transform has the benefit of not requiring manual imports. +## Common tool configurations +The following is an incomplete list of configurations to get started with Crank. -## Common tools and configurations -The following is an incomplete list of tool configurations to get started with JSX. +### [TypeScript](https://www.typescriptlang.org) -#### [TypeScript](https://www.typescriptlang.org) +TypeScript is a typed superset of JavaScript. Here’s the configuration you will need to set up automatic JSX transpilation. ```tsconfig.json { "compilerOptions": { - "jsx": "react-jsx" + "jsx": "react-jsx", "jsxImportSource": "@b9g/crank" } } @@ -103,41 +104,116 @@ The classic transform is supported as well. ```tsconfig.json { "compilerOptions": { - "target": "esnext", - TKTKTKTKTKTK + "jsx": "react", + "jsxFactory": "createElement", + "jsxFragmentFactory": "Fragment" } } ``` -Crank is written in TypeScript. Additional information about how to type components and use Crank types are provided in the [working with TypeScript guide](/guides/working-with-typescript). +Crank is written in TypeScript. Refer to [the guide on TypeScript](/guides/working-with-typescript) for more information about Crank types. -#### Babel -```babelrc.json -``` -You can install the “babel-preset-crank” package to set this up automatically. +```tsx +import type {Context} from "@b9g/crank"; +function *Timer(this: Context) { + let seconds = 0; + const interval = setInterval(() => { + seconds++; + this.refresh(); + }, 1000); + for ({} of this) { + yield
Seconds: {seconds}
; + } -#### ESBuild -``` + clearInterval(interval); +} ``` -#### Vite -``` +### [Babel](https://babeljs.io) + +Babel is a popular open-source JavaScript compiler which allows you to write code with modern syntax (including JSX) and run it in environments which do not support the syntax. + +Here is how to get Babel to transpile JSX for Crank. + +Automatic transform: +```.babelrc.json +{ + "plugins": [ + "@babel/plugin-syntax-jsx", + [ + "@babel/plugin-transform-react-jsx", + { + "runtime": "automatic", + "importSource": "@b9g/crank", + + "throwIfNamespace": false, + "useSpread": true + } + ] + ] +} ``` -#### ESLint +Classic transform: +```.babelrc.json +{ + "plugins": [ + "@babel/plugin-syntax-jsx", + [ + "@babel/plugin-transform-react-jsx", + { + "runtime": "class", + "pragma": "createElement", + "pragmaFrag": "''", + + "throwIfNamespace": false, + "useSpread": true + } + ] + ] +} ``` + +### [ESLint](https://eslint.org) + +ESLint is a popular open-source tool for analyzing and detecting problems in JavaScript code. + +Crank provides a configuration preset for working with ESLint under the package name `eslint-plugin-crank`. + +```bash +npm i eslint eslint-plugin-crank ``` -Crank provides +In your eslint configuration: -#### Astro.build +```.eslintrc.json +{ + "extends": ["plugin:crank/recommended"] +} ``` + +### [Astro](https://astro.build) + +Astro.js is a modern static site builder and framework. + +Crank provides an [Astro integration](https://docs.astro.build/en/guides/integrations-guide/) to enable server-side rendering and client-side hydration with Astro. + +```bash +npm i astro-crank ``` -## Avoiding JSX transpilation -```jsx -import {renderer} from "@b9g/crank/dom"; +In your `astro.config.mjs`. + +```astro.config.mjs +import {defineConfig} from "astro/config"; +import crank from "astro-crank"; + +// https://astro.build/config +export default defineConfig({ + integrations: [crank()], +}); ``` -If you do not want to use JSX, you can use the JavaScript friendly tagged template function instead. +## Shovel +A full-stack framework is in the works for Crank. Stay tuned. diff --git a/website/documents/guides/02-elements.md b/website/documents/guides/02-elements.md index 5ed0385de..a6007bfd8 100644 --- a/website/documents/guides/02-elements.md +++ b/website/documents/guides/02-elements.md @@ -1,30 +1,74 @@ --- -id: elements title: Elements and Renderers --- -**Note:** If you’re familiar with how elements work in React, you may want to skip ahead to [the guide on components](./components). Elements in Crank work almost exactly as they do in React. +**Note:** If you’re familiar with how elements work in React, you may want to skip ahead to [the guide on components](./components). Elements in Crank work almost exactly like they do in React. -## JSX +## Transpiling JSX +Crank works with [JSX](https://facebook.github.io/jsx/), a well-supported, XML-like syntax extension to JavaScript. -Crank is designed to be used with [JSX](https://facebook.github.io/jsx/), a well-supported, XML-like syntax extension to JavaScript. Most popular JavaScript transpilers support JSX transforms as an out-of-the-box feature. These transpilers work by transforming JSX expressions into function calls, and by convention, this has usually been a function called `createElement()`. For example, in the following code, the JSX expression assigned to `el` transpiles to the `createElement()` call assigned to `el1`. +### Two types of JSX transpilation +Historically speaking, there are two ways to transform JSX: the *classic* and *automatic* transforms. Crank supports both formats. + +The classic transform turns JSX elements into `createElement()` calls. ```jsx /** @jsx createElement */ import {createElement} from "@b9g/crank"; const el =
An element
; -// transpiles to: -const el1 = createElement("div", {id: "element"}, "An element"); ``` -The `createElement()` function provided by Crank returns an *element*, a JavaScript object. Elements on their own don’t do anything special; instead, we use special classes called *renderers* to interpret elements and produce DOM nodes, HTML strings, WebGL-backed scene graphs, or whatever else you can think of. +Transpiles to: + +```js +import {createElement} from "@b9g/crank"; + +const el = createElement("div", {id: "element"}, "An element"); +``` + +Identifiers like `createElement`, `Fragment` must be manually imported. -Crank ships with two renderer subclasses for the two common web development use-cases: one for managing DOM nodes, available through the module `@b9g/crank/dom`, and one for creating HTML strings, available through the module `@b9g/crank/html`. You can use these modules to render interactive user interfaces in the browser and HTML responses on the server. +The automatic transform turns JSX elements into function calls from an automatically imported namespace. + +```jsx +/** @jsxImportSource @b9g/crank */ + +const profile = ( +
+ +

{[user.firstName, user.lastName].join(" ")}

+
+); + +``` +Transpiles to: + +```js +import { jsx as _jsx } from "@b9g/crank/jsx-runtime"; +import { jsxs as _jsxs } from "@b9g/crank/jsx-runtime"; + +const profile = _jsxs("div", { + children: [ + _jsx("img", { + src: "avatar.png", + "class": "profile", + }), + _jsx("h3", { + children: [user.firstName, user.lastName].join(" "), + }), + ], +}); + +``` + +The automatic transform has the benefit of not requiring manual imports. Beyond this fact, there is no difference between the two transforms, and the `_jsx()`/`_jsxs()` functions are wrappers around `createElement()`. + +## Renderers + +Crank ships with two renderer subclasses for the web: one for managing DOM nodes in a front-end application, available through the module `@b9g/crank/dom`, and one for creating HTML strings, available through the module `@b9g/crank/html`. You can use these modules to render interactive user interfaces in the browser and HTML responses on the server. ```jsx -/** @jsx createElement */ -import {createElement} from "@b9g/crank"; import {renderer as DOMRenderer} from "@b9g/crank/dom"; import {renderer as HTMLRenderer} from "@b9g/crank/html"; @@ -40,9 +84,9 @@ console.log(html); //
Hello world
## The Parts of an Element -![Image of a JSX element](../static/parts-of-jsx.svg) +![Image of a JSX element](/static/parts-of-jsx.svg) -An element can be thought of as having three main parts: a *tag*, *props* and *children*. These roughly correspond to the syntax for HTML, and for the most part, you can copy-paste HTML into JSX-flavored JavaScript and have things work as you would expect. The main difference is that JSX has to be well-balanced like XML, so void tags must have a closing slash (`
` not `
`). Also, if you forget to close an element or mismatch opening and closing tags, the parser will throw an error, whereas HTML can be unbalanced or malformed and mostly still work. +An element can be thought of as having three main parts: a *tag*, *props* and *children*. These roughly correspond to the syntax for HTML, and for the most part, you can copy-paste HTML into JSX-flavored JavaScript and have things work as you would expect. The main difference is that JSX has to be well-balanced like XML, so void tags must have a closing slash (`
` not `
`). Also, if you forget to close an element or mismatch opening and closing tags, the parser will throw an error, whereas HTML can be unbalanced or malformed and mostly still work. ### Tags Tags are the first part of a JSX element expression, and can be thought of as the “name” or “type” of the element. JSX transpilers pass the tag of an element to the resulting `createElement()` call as its first argument. @@ -59,7 +103,7 @@ const componentEl1 = createElement(Component, null); By convention, JSX parsers treat lowercase tags (`
`) as strings and capitalized tags (``) as variables. When a tag is a string, this signifies that the element will be handled by the renderer. We call these types of elements *host* or *intrinsic* elements, and for both of the web renderers, these correspond exactly to actual HTML elements, like `
` or ``. -As we’ll see later, elements can also have tags which are functions, in which case the behavior of the element is defined not by the renderer but by the execution of the referenced function. Elements with function tags are called *component elements*. +As we’ll see later, elements can also have tags which are functions, in which case the behavior of the element is defined not by the renderer but by the execution of the referenced function. Elements with function tags are called *component* elements. ### Props JSX transpilers combine the attribute-like `key="value"` syntax to a single object for each element. We call this object the *props* object, short for “properties.” @@ -75,13 +119,17 @@ console.log(el.props); // {id: "my-id", "class": "my-class"} The value of each prop is a string if the string-like syntax is used (`key="value"`), or it can be an interpolated JavaScript expression by placing the value in curly brackets (`key={value}`). You can use props to “pass” values into host and component elements, similar to how you “pass” arguments into functions when invoking them. -If you already have an object that you want to use as props, you can use the special JSX `...` syntax to “spread” it into an element. This works similarly to [ES6 spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax). +### Spread props + +You can use the special JSX `...` syntax to “spread” it into an element. This works similarly to [ES6 spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax). ```jsx const props = {id: "1", src: "https://example.com/image", alt: "An image"}; const el = ; // transpiles to: const el1 = createElement("img", {...props, id: "2"}); + +console.log(el.props); // {id: "2", src: "https://example.com/image", alt: "An image"} ``` ### Children @@ -106,7 +154,9 @@ console.log(list.props.children.length); // 2 By default, JSX parsers interpret the contents of elements as strings. So for instance, in the JSX expression `

Hello world

`, the children of the `

` element would be the string `"Hello world"`. -However, just as with prop values, you can use curly brackets to interpolate JavaScript expressions into an element’s children. Besides elements and strings, almost every value in JavaScript can participate in an element tree. Numbers are rendered as strings, and the values `null`, `undefined`, `true` and `false` are erased, allowing you to render things conditionally using boolean expressions. +However, just as with prop values, you can use curly brackets to interpolate JavaScript expressions into an element’s children. Besides elements and strings, almost every value in JavaScript can participate in an element tree. + +Numbers are rendered as strings. The values `null`, `undefined`, `true` and `false` are erased, so you can conditionally render items with short-circuit evaluation or conditional expressions. ```jsx const el =

{"a"}{1 + 1}{true}{false}{null}{undefined}
; @@ -115,7 +165,7 @@ renderer.render(el, document.body); console.log(document.body.innerHTML); //
a2
``` -Crank also allows arbitrarily nested iterables of values to be interpolated as children, so, for instance, you can insert arrays or sets of elements into element trees. +Crank allows arbitrarily nested iterables of values to be interpolated as children, so, for instance, you can insert arrays or sets of elements into element trees. ```jsx const arr = [1, 2, 3]; diff --git a/website/documents/guides/03-components.md b/website/documents/guides/03-components.md index 865e2784d..58c388d91 100644 --- a/website/documents/guides/03-components.md +++ b/website/documents/guides/03-components.md @@ -2,10 +2,10 @@ title: Components --- -So far, we’ve only seen and used host elements, but eventually, we’ll want to group these elements into reusable *components*. Crank uses plain old JavaScript functions to define components, and we will see how it uses the different kinds of function types to allow developers to write reusable, stateful and interactive components. +So far, we’ve only seen and used *host elements*, lower-case elements like `` or `
`, which correspond to HTML. Eventually, we’ll want to group these elements into reusable *components*. Crank uses plain old JavaScript functions to define components. The type of the function determines the component’s behavior. ## Basic Components -The simplest kind of component is a *function component*. When rendered, the function is invoked with the props of the element as its first argument, and the return value of the function is recursively rendered as the element’s children. +The simplest kind of component is a *function component*. When rendered, the function is invoked with the props of the element as its first argument, and the return value of the function is rendered as the element’s children. ```jsx live import {renderer} from "@b9g/crank/dom"; @@ -16,66 +16,62 @@ function Greeting({name}) { renderer.render(, document.body); ``` -Component elements can be passed children just as host elements can. The `createElement()` function will add children to the props object under the name `children`, and it is up to the component to place them somewhere in the returned element tree. If you don’t use the `children` prop, it will not appear in the rendered output. +## Component children +Component elements can have children just like host elements. The `createElement()` function will add children to the props object under the name `children`, and it is up to the component to place the children somewhere in the returned element tree, otherwise it will not appear in the rendered output. ```jsx live import {renderer} from "@b9g/crank/dom"; -function Greeting({name, children}) { +function Details({summary, children}) { return ( -
- Message for {name}: {children} -
+
+ {summary} + {children} +
); } renderer.render( - - Howdy! - , +
+
Hello world
+
, document.body, ); ``` +The type of children is unknown, i.e. it could be an array, an element, or whatever else the caller passes in. + ## Stateful Components Eventually, you’ll want to write components with local state. In Crank, we use [generator functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) to do so. These types of components are referred to as *generator components*. -```jsx +A generator function is declared using `function *` syntax, and its body can contain one or more `yield` expressions. + +```jsx live +import {renderer} from "@b9g/crank/dom"; + function *Counter() { let count = 0; while (true) { count++; yield (
- You have updated this component {count} {count === 1 ? "time" : "times"} + You have updated this component {count} time{count !== 1 && "s"}.
); } } -renderer.render(, document.body); -console.log(document.body.innerHTML); -// "
You have updated this component 1 time
" -renderer.render(, document.body); -console.log(document.body.innerHTML); -// "
You have updated this component 2 times
" -renderer.render(, document.body); renderer.render(, document.body); renderer.render(, document.body); -console.log(document.body.innerHTML); -// "
You have updated this component 5 times
" -renderer.render(null, document.body); -console.log(document.body.innerHTML); -// "" renderer.render(, document.body); -console.log(document.body.innerHTML); -// "
You have updated this component 1 time
" ``` -By yielding elements rather than returning them, we can make components stateful using variables in the generator’s local scope. Crank uses the same diffing algorithm which reuses DOM nodes to reuse generator objects, so that their executions are preserved between renders. Every time a generator component is rendered, Crank resumes the generator and executes the generator until the next `yield`. The yielded expression, usually an element, is then rendered as the element’s children, just as if it were returned from a function component. +By yielding elements rather than returning them, components can be made stateful using variables in the generator’s local scope. Crank uses the same diffing algorithm which reuses DOM nodes to reuse generator objects, so there will only be one execution of a generator component for a given element in the tree. -### Contexts -In the preceding example, the `Counter` component’s local state changed when it was rerendered, but we may want to write components which update themselves according to timers or events instead. Crank allows components to control their own execution by passing in an object called a *context* as the `this` keyword of each component. Component contexts provide several utility methods, most important of which is the `refresh` method, which tells Crank to update the related component instance in place. +## The Crank Context +In the preceding example, the component’s local state was updated directly when the generator was executed. This is of limited value insofar as what we usually want want is to update according to events or timers. + +Crank allows components to control their own execution by passing in an object called a *context* as the `this` keyword of each component. Contexts provide several utility methods, the most important of which is the `refresh()` method, which tells Crank to update the related component instance in place. ```jsx function *Timer() { @@ -97,119 +93,74 @@ function *Timer() { } ``` -This `Timer` component is similar to the `Counter` one, except now the state (the local variable `seconds`) is updated in a `setInterval()` callback, rather than when the component is rerendered. Additionally, the `refresh()` method is called to ensure that the generator is stepped through whenever the `setInterval()` callback fires, so that the rendered DOM actually reflects the updated `seconds` variable. +This `` component is similar to the `` one, except now the state (the local variable `seconds`) is updated in a `setInterval()` callback, rather than when the component is rerendered. Additionally, the `refresh()` method is called to ensure that the generator is stepped through whenever the `setInterval()` callback fires, so that the rendered DOM actually reflects the updated `seconds` variable. One important detail about the `Timer` example is that it cleans up after itself with `clearInterval()` in the `finally` block. Behind the scenes, Crank will call the `return()` method on an element’s generator object when it is unmounted. -### Props Updates -The generator components we’ve seen so far haven’t used props. Generator components can accept props as their first parameter just like regular function components. - -```jsx -function *LabeledCounter({message}) { - let count = 0; - while (true) { - count++; - yield
{message} {count}
; - } -} - -renderer.render( - , - document.body, -); - -console.log(document.body.innerHTML); // "
The count is now: 1
" - -renderer.render( - , - document.body, -); - -console.log(document.body.innerHTML); // "
The count is now: 2
" +## The Render Loop -renderer.render( - , - document.body, -); - -// WOOPS! -console.log(document.body.innerHTML); // "
The count is now: 3
" -``` +The generator components we’ve seen so far haven’t used props. They’ve also used while (true) loops, which was done mainly for learning purposes. In actuality, Crank contexts are iterables of props, so you can `for...of` iterate through them. -This mostly works, except we have a bug where the component keeps yielding elements with the initial message even though a new message was passed in via props. We can make sure props are kept up to date by iterating over the context: +```jsx live +import {renderer} from "@b9g/crank/dom"; +function *Timer({message}) { + let seconds = 0; + const interval = setInterval(() => { + seconds++; + this.refresh(); + }, 1000); -```jsx -function *Counter({message}) { - let count = 0; for ({message} of this) { - count++; yield ( -
{message} {count}
+
{message}: {seconds}
); } -} - -renderer.render( - , - document.body, -); -console.log(document.body.innerHTML); // "
The count is now: 1
" + clearInterval(interval); +} renderer.render( - , + , document.body, ); -console.log(document.body.innerHTML); // "
Le décompte est maintenant: 2
" +setTimeout(() => { + renderer.render( + , + document.body, + ); +}, 4500); ``` -By replacing the `while` loop with a `for…of` loop which iterates over `this`, you can get the latest props each time the generator is resumed. This is possible because contexts are an iterable of the latest props passed to components. +The loop created by iterating over contexts is called the *render loop*. By replacing the `while` loop with a `for...of` loop which iterates over `this`, you can get the latest props each time the generator is resumed. -### Comparing Old and New Props +The render loop has additional advantages over while loops. For instance, you can place cleanup code directly after the loop. The render loop will also throw errors if it has been iterated without a yield, to prevent infinite loops. -One Crank idiom we see in the preceding example is that we overwrite the variables declared via the generator’s parameters with the destructuring expression in the `for…of` statement. This is an easy way to make sure those variables stay in sync with the current props of the component. However, there is no requirement that you must always overwrite old props in the `for` expression, meaning you can assign new props to a different variable and compare them against the old props. +One Crank idiom you may have noticed is that we define props in component parameters, and overwrite them using a destructuring expression in the `for...of` statement. This is an easy way to make sure those variables stay in sync with the current props of the component. For this reason, even if your component has no props, it is idiomatic to use a render loop. ```jsx live import {renderer} from "@b9g/crank/dom"; +function *Counter() { + let count = 0; + const onclick = () => { + count++; + this.refresh(); + }; -function *Greeting({name}) { - yield
Hello {name}.
; - for (const {name: newName} of this) { - if (name === newName) { - yield ( -
Hello again {newName}.
- ); - } else { - yield ( -
Goodbye {name} and hello {newName}.
- ); - } - - name = newName; - } -} - -function *App() { - let i = 0; for ({} of this) { - const name = (Math.floor(i++ / 2) % 2) === 0 ? "Alice" : "Bob"; yield ( -
- - -
+ ); } } -renderer.render(, document.body); +renderer.render(, document.body); ``` -The fact that state is just local variables allows us to blur the lines between props and state, in a way that is easy to understand and without lifecycle methods like `componentWillUpdate` from React. With generators and `for` loops, comparing old and new props is as easy as comparing adjacent elements of an array. - ## Default Props -You may have noticed in the preceding examples that we used [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring) on the props parameter for convenience. You can further assign default values to specific props by using JavaScript’s default value syntax. +You may have noticed in the preceding examples that we used [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring) on the props parameter for convenience. You can further assign default values to specific props using JavaScript’s default value syntax. ```jsx function Greeting({name="World"}) { @@ -219,15 +170,16 @@ function Greeting({name="World"}) { renderer.render(, document.body); // "
Hello World
" ``` -This syntax works well for function components, but for generator components, you should make sure that you use the same default value in both the parameter list and the `for` statement. +This syntax works well for function components, but for generator components, you should make sure that you use the same default value in both the parameter list and the loop. A mismatch in the default values for a prop between these two positions may cause surprising behavior. -```jsx +```jsx live +import {renderer} from "@b9g/crank/dom"; function *Greeting({name="World"}) { yield
Hello, {name}
; for ({name="World"} of this) { yield
Hello again, {name}
; } } -``` -A mismatch in the default values for a prop between these two positions may cause surprising behavior. +renderer.render(, document.body); +``` diff --git a/website/documents/guides/04-handling-events.md b/website/documents/guides/04-handling-events.md index 0d63dc3d1..4dbf3c4c7 100644 --- a/website/documents/guides/04-handling-events.md +++ b/website/documents/guides/04-handling-events.md @@ -2,57 +2,66 @@ title: Handling Events --- -Most web applications require some measure of interactivity, where the user interface updates according to input. To facilitate this, Crank provides two APIs for listening to events on rendered DOM nodes. +Most web applications require some measure of interactivity, where the user interface updates according to input. To facilitate this, Crank provides several ways to listen to and trigger events. -## DOM onevent Props -You can attach event callbacks to host element directly using onevent props. These props start with `on`, are all lowercase, and correspond to the properties as specified according to the DOM’s [GlobalEventHandlers mixin API](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers). By combining event props, local variables and `this.refresh`, you can write interactive components. +## DOM Event Props +You can attach event callbacks to host element directly using event props. These props start with `on`, are all lowercase, and correspond to the event type (`onclick`, `onkeydown`). By combining event props, local variables and `this.refresh()`, you can write interactive components. -```jsx -function *Clicker() { +```jsx live +import {renderer} from "@b9g/crank/dom"; +function *Counter() { let count = 0; - const handleClick = () => { + const onclick = () => { count++; this.refresh(); }; - while (true) { + for ({} of this) { yield ( -
- The button has been clicked {count} {count === 1 ? "time" : "times"}. - -
+ ); } } + +renderer.render(, document.body); ``` ## The EventTarget Interface -As an alternative to the onevent props API, Crank contexts also implement the same `EventTarget` interface used by the DOM. The `addEventListener` method attaches a listener to a component’s rendered DOM node or nodes. +As an alternative to event props, Crank contexts implement the same `EventTarget` interface used by the DOM. The `addEventListener()` method attaches a listener to a component’s root DOM node. -```jsx -function *Clicker() { +```jsx live +import {renderer} from "@b9g/crank/dom"; +function *Counter() { let count = 0; this.addEventListener("click", () => { count++; this.refresh(); }); - while (true) { + for ({} of this) { yield ( - + ); } } + +renderer.render(, document.body); ``` -The local state `count` is now updated in the event listener, which triggers when the rendered button is actually clicked. +The context’s `addEventListener()` method attaches to the top-level node or nodes which each component renders, so if you want to listen to events on a nested node, you must use event delegation. -**NOTE:** When using the context’s `addEventListener` method, you do not have to call the `removeEventListener` method if you merely want to remove event listeners when the component is unmounted. This is done automatically. +While the `removeEventListener()` method is implemented, you do not have to call the `removeEventListener()` method if you merely want to remove event listeners when the component is unmounted. -The context’s `addEventListener` method only attaches to the top-level node or nodes which each component renders, so if you want to listen to events on a nested node, you must use event delegation. +Because the event listener is attached to the outer `div`, we have to filter events by `ev.target.tagName` in the listener to make sure we’re not incrementing `count` based on clicks which don’t target the `button` element. + +```jsx live +import {renderer} from "@b9g/crank/dom"; -```jsx -function *Clicker() { +function *Counter() { let count = 0; this.addEventListener("click", (ev) => { if (ev.target.tagName === "BUTTON") { @@ -61,30 +70,31 @@ function *Clicker() { } }); - while (true) { + for ({} of this) { yield (
- The button has been clicked {count} {count === 1 ? "time" : "times"}. - +

The button has been clicked {count} time{count !== 1 && "s"}.

+
); } } -``` -Because the event listener is attached to the outer `div`, we have to filter events by `ev.target.tagName` in the listener to make sure we’re not incrementing `count` based on clicks which don’t target the `button` element. +renderer.render(, document.body); +``` -## onevent vs EventTarget -The props-based onevent API and the context-based EventTarget API both have their advantages. On the one hand, using onevent props means you don’t have to filter events by target. You register them on exactly the element you’d like to listen to. +## Event props vs EventTarget +The props-based event API and the context-based EventTarget API both have their advantages. On the one hand, using event props means you can listen to exactly the element you’d like to listen to. -On the other, using the `addEventListener` method allows you to take full advantage of the EventTarget API, which includes registering passive event listeners or listeners which are dispatched during the capture phase. Additionally, the EventTarget API can be used without referencing or accessing the child elements which a component renders, meaning you can use it to listen to components which are passed children, or in utility functions which don’t have access to produced elements. +On the other hand, using the `addEventListener` method allows you to take full advantage of the EventTarget API, which includes registering passive event listeners, or listeners which are dispatched during the capture phase. Additionally, the EventTarget API can be used without referencing or accessing the child elements which a component renders, meaning you can use it to listen to elements nested in other components. Crank supports both API styles for convenience and flexibility. ## Dispatching Events Crank contexts implement the full EventTarget interface, meaning you can use [the `dispatchEvent` method](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent) and [the `CustomEvent` class](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) to dispatch custom events to ancestor components: -```jsx +```jsx live +import {renderer} from "@b9g/crank/dom"; function MyButton(props) { this.addEventListener("click", () => { this.dispatchEvent(new CustomEvent("mybuttonclick", { @@ -100,9 +110,9 @@ function MyButton(props) { function MyButtons() { return [1, 2, 3, 4, 5].map((i) => ( -
+

Button {i} -

+

)); } @@ -113,109 +123,128 @@ function *MyApp() { this.refresh(); }); - while (true) { + for ({} of this) { yield (
-
Last pressed id: {lastId == null ? "N/A" : lastId}
+

+ {lastId == null + ? "No buttons have been pressed." + : `The last pressed button had an id of ${lastId}`} +

); } } -``` -`MyButton` is a function component which wraps a `button` element. It dispatches a `CustomEvent` whose type is `"mybuttonclick"` when it is pressed, and whose `detail` property contains data about the ID of the clicked button. This event is not triggered on the underlying DOM nodes; instead, it can be listened for by parent component contexts using event capturing and bubbling, and in the example, the event propagates and is handled by the `MyApp` component. Using custom events and event bubbling allows you to encapsulate state transitions within component hierarchies without the need for complex state management solutions used in other frameworks like Redux or VueX. +renderer.render(, document.body); +``` -The preceding example also demonstrates a slight difference in the way the `addEventListener` method works in function components compared to generator components. With generator components, listeners stick between renders, and will continue to fire until the component is unmounted. However, with function components, because the `addEventListener` call would be invoked every time the component is rerendered, we remove and add listeners for each render. This allows function components to remain stateless while still listening for and dispatching events. +`` is a function component which wraps a ` - -
- ); - } else { - yield ( -
- - -
- ); - } + for ({} of this) { + yield ( + + ); } } + +renderer.render(, document.body); ``` -While frameworks like React notice the absence of the boolean `checked` prop between renders and mutate the input element, Crank does not. This means that the input element can only ever go from unchecked to checked, and not the other way around. To fix the above example, you would need to make sure the `checked` prop is always passed to the `input` element. +Using custom events and event bubbling allows you to encapsulate state transitions within component hierarchies without the need for complex state management solutions used in other frameworks like Redux or VueX. -```jsx -function *App() { - let checked = false; - this.addEventListener("click", (ev) => { - if (ev.target.tagName === "BUTTON") { - checked = !checked; - this.refresh(); - } - }); +## Form Elements + +Because Crank uses explicit state updates, it doesn’t need a concept like “controlled” `value` vs “uncontrolled” `defaultValue` props in React. No update means the value is uncontrolled. + +```jsx live +import {renderer} from "@b9g/crank/dom"; +function *Form() { + let reset = false; + const onreset = () => { + reset = true; + this.refresh(); + }; - this.addEventListener("input", (ev) => ev.preventDefault()); + const onsubmit = (ev) => { + ev.preventDefault(); + }; - while (true) { + for ({} of this) { yield ( -
- - -
+
+ +

+ +

+
); } } + +renderer.render(
, document.body); ``` -This design decision means that we now have a way to make the same element prop both “uncontrolled” and “controlled” for an element. Here, for instance, is an input element which is uncontrolled, except that it resets when the button is clicked. +If your component is updating for other reasons, you can use the special property `$static` to prevent the input element from updating. -```jsx -function* ResettingInput() { - let reset = true; - this.addEventListener("click", ev => { - if (ev.target.tagName === "BUTTON") { - reset = true; - this.refresh(); - } - }); +```jsx live +import {renderer} from "@b9g/crank/dom"; +function *Form() { + let reset = false; + const onreset = () => { + reset = true; + this.refresh(); + }; - while (true) { - const reset1 = reset; + const onsubmit = (ev) => { + ev.preventDefault(); + }; + + setInterval(() => { + this.refresh(); + }, 1000); + + for ({} of this) { + const currentReset = reset; reset = false; yield ( -
- - {reset1 ? : } -
+ + +

+ +

+
); } } -``` -In the above example, we use the `reset` flag to check whether we need to set the `value` prop of the underlying input DOM element, and we omit the `value` prop when we aren’t performing a reset. Because the prop is not cleared when absent from the virtual element’s props, Crank leaves it alone. Crank’s approach means we do not need special alternative props for uncontrolled behavior, and we can continue to use virtual element rendering over raw DOM mutations in those circumstances where we do need control. +renderer.render(
, document.body); +``` diff --git a/website/documents/guides/05-async-components.md b/website/documents/guides/05-async-components.md index da6247e78..80f092ae5 100644 --- a/website/documents/guides/05-async-components.md +++ b/website/documents/guides/05-async-components.md @@ -2,133 +2,83 @@ title: Async Components --- -## Async Function Components -So far, every component we’ve seen has worked synchronously, and Crank will respect this as an intentional decision by the developer by keeping the entire process of rendering synchronous from start to finish. However, modern JavaScript includes promises and `async`/`await`, which allow you to write concurrently executing code as if it were synchronous. To facilitate these features, Crank allows components to be asynchronous functions as well, and we call these components, *async function components*. +So far, every component we’ve seen has been a sync function or sync generator component. Crank processes element trees containing synchronous components instantly, ensuring that by the time `renderer.render()` or `this.refresh()` completes execution, rendering will have finished, and the DOM will have been updated. -```jsx -async function IPAddress () { - const res = await fetch("https://api.ipify.org"); - const address = await res.text(); - return
Your IP Address: {address}
; +Nevertheless, a JavaScript component framework would not be complete without a way to work with promises. Luckily, Crank also allows any component to be async the same way you would make any function asynchronous, by adding an `async` before the `function` keyword. Both *async function* and *async generator components* are supported. This feature means you can `await` promises in the process of rendering in virtually any component. + +```jsx live +import {renderer} from "@b9g/crank/dom"; +async function Definition({word}) { + // API courtesy https://dictionaryapi.dev + const res = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`); + const data = await res.json(); + const {phonetic, meanings} = data[0]; + const {partOfSpeech, definitions} = meanings[0]; + const {definition} = definitions[0]; + return <> +

{word} {phonetic}

+

{partOfSpeech}. {definition}

+ {/*
{JSON.stringify(data, null, 4)}
*/} + ; } -(async () => { - await renderer.render(, document.body); - console.log(document.body.innerHTML); //
Your IP Address: 127.0.0.1
-})(); +await renderer.render(, document.body); ``` -When Crank renders an async component anywhere in the tree, the entire process becomes asynchronous. Concretely, this means that `renderer.render` or `this.refresh` calls return a promise which fulfills when rendering has finished. It also means that no actual DOM updates will be triggered until this moment. +When rendering is async, `renderer.render()` and `this.refresh()` will return promises which settle when rendering has finished. ### Concurrent Updates -Because async function components can be rerendered while they are still pending, Crank implements a couple rules to make concurrent updates predictable and performant: +The nature of declarative rendering means that async components can be rerendered while they are still pending. Therefore, Crank implements a couple rules to make concurrent updates predictable and performant: -1. There can only be one pending run of an async function component at the same time for an element in the tree. If the same async component is rerendered concurrently while it is still pending, another call is enqueued with the latest props. +1. There can only be one pending run of an async component at the same time for an element in the tree. If the same async component is rerendered concurrently while it is still pending, another call is enqueued with the updated props. -```jsx +```jsx live +import {renderer} from "@b9g/crank/dom"; async function Delay ({message}) { await new Promise((resolve) => setTimeout(resolve, 1000)); return
{message}
; } -(async () => { - const p1 = renderer.render(, document.body); - console.log(document.body.innerHTML); // "" - await p1; - console.log(document.body.innerHTML); // "
Run 1
" - const p2 = renderer.render(, document.body); - // These renders are enqueued because the second render is still pending. - const p3 = renderer.render(, document.body); - const p4 = renderer.render(, document.body); - console.log(document.body.innerHTML); // "
Run 1
" - await p2; - console.log(document.body.innerHTML); // "
Run 2
" - // By the time the third render fulfills, the fourth render has already completed. - await p3; - console.log(document.body.innerHTML); // "
Run 4
" - await p4; - console.log(document.body.innerHTML); // "
Run 4
" -})(); +renderer.render(, document.body); +await p1; +renderer.render(, document.body); +// These renders are enqueued because the second render is still pending. +// The third render is dropped because when the second run fulfills, there is +// already a fourth run which provides the latest props. +renderer.render(, document.body); +renderer.render(, document.body); ``` -In the preceding example, at no point is there more than one simultaneous call to the `Delay` component, despite the fact that it is rerendered concurrently for its second through fourth renders. And because these renderings are enqueued, only the second and fourth renderings have any effect. This is because the element is busy with the second render by the time the third and fourth renderings are requested, and then, only the fourth rendering is actually executed because third rendering’s props are obsolete by the time the component is ready to update again. This behavior allows async components to always be kept up-to-date without producing excess calls to the function. +In the preceding example, at no point is there more than one simultaneous call to the `` component, despite the fact that it is rerendered concurrently for its second through fourth renders. And because these renderings are enqueued, only the second and fourth renderings have any effect. This is because the element is busy with the second render by the time the third and fourth renderings are requested, and then, only the fourth rendering is actually executed because third rendering’s props are obsolete by the time the component is ready to update again. This behavior allows async components to always be kept up-to-date without producing excess calls. 2. If two different async components are rendered in the same position, the components are raced. If the earlier component fulfills first, it shows until the later component fulfills. If the later component fulfills first, the earlier component is never rendered. -```jsx +```jsx live +import {renderer} from "@b9g/crank/dom"; + async function Fast() { - await new Promise((resolve) => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 1000)); return Fast; } async function Slow() { - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 2000)); return Slow; } -(async () => { - const p1 = renderer.render(
, document.body); - const p2 = renderer.render(
, document.body); - await p1; - console.log(document.body.innerHTML); // "
Fast
" - await p2; - console.log(document.body.innerHTML); // "
Slow
" - await new Promise((resolve) => setTimeout(resolve, 2000)); - console.log(document.body.innerHTML); // "
Slow
" -})(); - -(async () => { - const p1 = renderer.render(
, document.body); - const p2 = renderer.render(
, document.body); - await p1; - console.log(document.body.innerHTML); // "
Fast
" - await p2; - console.log(document.body.innerHTML); // "
Fast
" - await new Promise((resolve) => setTimeout(resolve, 2000)); - console.log(document.body.innerHTML); // "
Fast
" -})(); +// TODO: flip the order of these calls and watch the behavior. +renderer.render(, document.body); +renderer.render(, document.body); ``` -As we’ll see later, this ratcheting effect becomes useful for rendering fallback states for async components. - - +As we’ll see later, this “ratcheting” effect becomes useful for rendering fallback states for async components. ## Async Generator Components -Just as you can write stateful components with sync generator functions, you can also write stateful *async* components with *async generator functions*. +Just as you can write stateful components with sync generator functions, you can also write *stateful* async components with async generator functions. -```jsx -async function *AsyncLabeledCounter ({message}) { - let count = 0; - for await ({message} of this) { - yield
Loading...
; - await new Promise((resolve) => setTimeout(resolve, 1000)); - count++; - yield
{message} {count}
; - } -} - -(async () => { - await renderer.render( - , - document.body, - ); - console.log(document.body.innerHTML); //
Loading...
- await new Promise((resolve) => setTimeout(resolve, 2000)); - console.log(document.body.innerHTML); //
The count is now: 1
- await renderer.render( - , - document.body, - ); - console.log(document.body.innerHTML); //
Loading...
- await new Promise((resolve) => setTimeout(resolve, 2000)); - console.log(document.body.innerHTML); //
The count is now: 2
- await new Promise((resolve) => setTimeout(resolve, 2000)); - console.log(document.body.innerHTML); //
The count is now: 2
-})(); +```jsx live +import {renderer} from "@b9g/crank/dom"; +renderer.render(, document.body); ``` `AsyncLabeledCounter` is an async version of the `LabeledCounter` example introduced in [the section on props updates](./components#props-updates). This example demonstrates several key differences between sync and async generator components. Firstly, rather than using `while` or `for…of` loops as with sync generator components, we now use [a `for await…of` loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of). This is possible because contexts are not just an *iterable* of props, but also an *async iterable* of props as well. diff --git a/website/documents/guides/12-jsx-template-tag.md b/website/documents/guides/12-jsx-template-tag.md index c101e58ae..f1d59bf13 100644 --- a/website/documents/guides/12-jsx-template-tag.md +++ b/website/documents/guides/12-jsx-template-tag.md @@ -7,59 +7,78 @@ function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Temp as an alternative to JSX syntax. The main advantage of using a template tag is that your code can run directly in browsers without having to be transpiled. -```html +## A single-file Crank application. + +```index.html - - - - TODO: Replace me - - -
- - + function Greeting({name="World"}) { + return jsx` +
Hello ${name}
+ `; + } + + renderer.render( + jsx`<${Greeting} name="Alice" />`, + document.getElementById("root"), + ); + + ``` A Crank application as a single HTML file. No transpilation required. -The JSX tag function can be imported from the module `"@b9g/crank/standalone"`. -This module exports everything from the root `@b9g/crank` module as well as the -`jsx` tag function, which is defined in the module `"@b9g/crank/jsx-tag"`. +## Installation -The modules are structured like this to prevent. Due to limitations with template tags, component -references in tags must be interpolated. +The JSX tag function can be imported from the module `@b9g/crank/standalone`. This module exports everything from the root `@b9g/crank` module as well as the `jsx` tag function, which is defined in the module `@b9g/crank/jsx-tag`. -```jsx -import {jsx} from "@b9g/crank/standalone"; +```js live +import {Fragment, jsx} from "@b9g/crank/standalone"; +import {jsx as jsx1} from "@b9g/crank/jsx-tag"; import {renderer} from "@b9g/crank/dom"; -function Greeting({name="World"}) { - return jsx` -
Hello ${name}
- `; +renderer.render(jsx` + <${Fragment}>, +
Hello world
+ +`, document.body); + +// console.log(jsx === jsx1); +``` + +In the future, we may use environment detection to automatically exports the correct `renderer`, which would make the `standalone` module truly “standalone.” + +## JSX Syntax + +The JSX template tag function is designed to replicate as much of JSX syntax and semantics as possible. + +Just like JSX syntax, the template version supports components, but they must be explicitly interpolated. + +```js +import {jsx} from "@b9g/crank/standalone"; +function Component() { + /* ... */ } -renderer.render(, document.body); -// Notice the ${} is necessary. JSX Syntax wins here. -renderer.render(jsx`<${Greeting} name="Bob" />`, document.body); +const syntaxEl = ; +const templateEl = jsx`<${Component} />`; ``` -## Comparing JSX syntax to JSX template tags +Component closing tags can be done in one of three styles: -JSX syntax +```js +import {jsx} from "@b9g/crank/standalone"; + +``` diff --git a/website/documents/index.md b/website/documents/index.md index ab5b9bdf6..f2d1b4d03 100644 --- a/website/documents/index.md +++ b/website/documents/index.md @@ -3,12 +3,17 @@ title: Crank.js description: "The Just JavaScript framework. Crank is a JavaScript / TypeScript library where you write components with functions, promises and generators." --- -Crank is a JavaScript / TypeScript library for building websites and apps. It is a framework where components are defined with plain old functions, including async and generator functions, which `yield` and `return` JSX templates. +## What is Crank? + +Crank is a JavaScript / TypeScript library for building websites and applications. It is a framework where components are defined with plain old functions, including async and generator functions, which `yield` and `return` JSX. ## Why is Crank “Just JavaScript?” -Many web frameworks claim to be “just JavaScript.” Few have as strong a claim as Crank. -It starts with the idea that you can write components with JavaScript’s built-in function syntaxes. +Many web frameworks claim to be “just JavaScript.” + +Few have as strong a claim as Crank. + +It starts with the idea that you can write components with *all* of JavaScript’s built-in function syntaxes. ```jsx live import {renderer} from "@b9g/crank/dom"; @@ -47,7 +52,7 @@ async function Definition({word}) { //renderer.render(, document.body); ``` -Promises can be awaited. Updates can be iterated. State and callbacks can be held in scope. Inside a component, JavaScript can be JavaScript. +Crank components work like normal JavaScript, using standard control-flow. Props can be destructured. Promises can be awaited. Updates can be iterated. State can be held in scope. The result is a simpler developer experience, where you spend less time writing framework integrations and more time writing vanilla JavaScript. @@ -55,8 +60,7 @@ The result is a simpler developer experience, where you spend less time writing ### Reason #1: It’s declarative -Crank works with JSX. It uses tried-and-tested virtual DOM algorithms. Simple -components can be defined with functions which return elements. +Crank works with JSX. It uses tried-and-tested virtual DOM algorithms. Simple components can be defined with functions which return elements. ```jsx live import {renderer} from "@b9g/crank/dom"; @@ -83,8 +87,7 @@ function RandomName() { renderer.render(, document.body); ``` -Don’t think JSX is vanilla enough? Crank provides a tagged template function -which does roughly the same thing. +Don’t think JSX is vanilla enough? Crank provides a tagged template function which does roughly the same thing. ```jsx live import {jsx} from "@b9g/crank/standalone"; @@ -199,6 +202,8 @@ renderer.render(, document.body); Components rerender based on explicit `refresh()` calls. This level of precision means you can be as messy as you need to be. +Never memoize a callback ever again. + ```jsx live import {renderer} from "@b9g/crank/dom"; @@ -258,29 +263,71 @@ renderer.render(, document.body); ### Reason #3: It’s promise-friendly. -Any component can be made asynchronous with the `async` keyword. As it turns out, one of the nicest ways to use `fetch()` is to call it and `await` the result. +Any component can be made asynchronous with the `async` keyword. This means you can await `fetch()` directly in a component, client or server. ```jsx live import {renderer} from "@b9g/crank/dom"; -async function QuoteOfTheDay() { - // Quotes API courtesy https://theysaidso.com - const res = await fetch("https://quotes.rest/qod.json"); - const quote = (await res.json())["contents"]["quotes"][0]; +async function Definition({word}) { + // API courtesy https://dictionaryapi.dev + const res = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`); + const data = await res.json(); + if (!Array.isArray(data)) { + return ( +
No definition found for {word}
+ ); + } + + const {phonetic, meanings} = data[0]; + const {partOfSpeech, definitions} = meanings[0]; + const {definition} = definitions[0]; return ( -
-
{quote.quote}
-
- — {quote.author} -
-
+
+

{word} {phonetic}

+

{partOfSpeech}. {definition}

+
); } -renderer.render(, document.body); +function *Dictionary() { + let word = ""; + const onsubmit = (ev) => { + ev.preventDefault(); + const formData = new FormData(ev.target); + const word1 = formData.get("word"); + if (word1.trim()) { + word = word1; + this.refresh(); + } + }; + + for ({} of this) { + yield ( + <> + +
+ {" "} + +
+
+ +
+ + {word && } + + ); + } +} + +renderer.render(, document.body); ``` -Async generator functions let you write components that are both async *AND* stateful. You can even race components to show temporary fallback states. +Async generator functions let you write components that are both async *and* stateful. Crank uses promises wherever it makes sense, and has a rich async execution model which allows you to do things like racing components to display loading states. ```jsx live import {renderer} from "@b9g/crank/dom"; diff --git a/website/examples/minesweeper.ts b/website/examples/minesweeper.ts new file mode 100644 index 000000000..12ea9657f --- /dev/null +++ b/website/examples/minesweeper.ts @@ -0,0 +1,137 @@ +import {jsx} from "@b9g/crank/standalone"; +import {renderer} from "@b9g/crank/dom"; + +function Hexagon({cx = 0, cy = 0, r, ...props}) { + if (!r) { + return null; + } + + const points = []; + for (let i = 0, a = 0; i < 6; i++, a += Math.PI / 3) { + points.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]); + } + + return jsx` + + `; +} + +function centerCoordsFor(cell, size) { + const colSpacing = (size * 3) / 2; + const rowSpacing = Math.sqrt(3) * size; + return { + cx: cell.col * colSpacing, + cy: cell.row * rowSpacing + (cell.col % 2 === 0 ? 0 : rowSpacing / 2), + }; +} + +function axialCoordsFor(cell) { + return {q: cell.col, r: cell.row - Math.floor(cell.col / 2)}; +} + +function HexagonalGrid({radius = 20, cells, testCell}) { + return cells.map((cell, i) => { + const onclick = () => { + console.log(neighborsOf(cell, cells)); + }; + const {cx, cy} = centerCoordsFor(cell, radius); + const {q, r} = axialCoordsFor(cell); + return jsx` + <${Hexagon} + r=${radius} + cx=${cx} cy=${cy} + fill="white" stroke="dodgerblue" + onclick=${onclick} + /> + ${cell.bombCount && !cell.bomb ? cell.bombCount : null} + ${cell.bomb && "!"} + + `; + }); +} + +function shuffle(arr) { + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + + return arr; +} + +function neighborsOf(cell, cells) { + const {q, r} = axialCoordsFor(cell); + const vectors = [ + [1, 0], + [0, 1], + [-1, 1], + [-1, 0], + [0, -1], + [1, -1], + ]; + const axialSet = new Set(vectors.map(([q1, r1]) => `${q + q1},${r + r1}`)); + return cells.filter((cell1) => { + const {q, r} = axialCoordsFor(cell1); + return axialSet.has(`${q},${r}`); + }); +} + +function* Minesweeper() { + const rows = 12, + cols = 15; + const cells = []; + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + cells.push({row, col, bomb: false, bombCount: 0}); + } + } + + for (const cell of shuffle(cells.slice()).slice(0, 30)) { + cell.bomb = true; + } + + for (const cell of cells) { + cell.bombCount = neighborsOf(cell, cells).reduce( + (a, c) => a + (c.bomb ? 1 : 0), + 0, + ); + } + + for ({} of this) { + yield jsx` + + <${HexagonalGrid} cells=${cells} radius=${15} /> + + `; + } +} + +renderer.render(jsx`<${Minesweeper} />`, document.body); diff --git a/website/examples/wizard.js b/website/examples/wizard.js new file mode 100644 index 000000000..953b457fa --- /dev/null +++ b/website/examples/wizard.js @@ -0,0 +1,58 @@ +import {renderer} from "@b9g/crank/dom"; + +function* Wizard() { + let step = 0; + const formData = new FormData(); + this.addEventListener("submit", (ev) => { + const isValid = ev.target.reportValidity(); + if (isValid) { + ev.preventDefault(); + const data = new FormData(ev.target); + for (const [key, value] of data) { + formData.append(key, value); + } + + // Code to handle form submission + step++; + this.refresh(); + } + }); + + for ({} of this) { + yield ( +
+ {step === 0 ? ( + <> + +
+ +
+ +
+ +
+
+ + + ) : step === 1 ? ( + <> + +
+