diff --git a/.gitignore b/.gitignore index f3c315a..4dec440 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,26 @@ -/target -/classes -/checkouts -pom.xml.asc -*.jar +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions *.class +*.jar +.pnp.* +.shadow-cljs +.yarn/* +/*-init.clj +/*-init.clj +/.cpcache /.lein-* /.nrepl-port -/*-init.clj +/.rebel_readline_history +/.shadow-cljs +/checkouts +/classes /doc/dist +/nashorn_code_cache +/node_modules /out /repl -/node_modules -/nashorn_code_cache -/.rebel_readline_history -/.cpcache -/.shadow-cljs \ No newline at end of file +/target +pom.xml.asc \ No newline at end of file diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..896c0ee --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,9 @@ +enableGlobalCache: true + +enableImmutableCache: false + +enableImmutableInstalls: false + +enableTelemetry: false + +nodeLinker: node-modules diff --git a/CHANGES.md b/CHANGES.md index 599d672..93b3207 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,85 @@ # Changelog # +## Version v2.10 + +- Add the `::rumext.v2/memo` metadata for simplify the common case for memoization +- Add the `::rumext.v2.props/expect` metadata for props checking; it + accepts a set of props for simple existence checkong or map with + predicates for simple type checking +- Add native js destructuring support on props + + +## Version v2.9.3 + +- bugfixes + +## Version v2.9.2 + +- bugfixes + +## Version v2.9.1 + +- bugfixes + + +## Version v2.9 + +- Make the library more lightweight removing unnecesary and duplicated code +- Add the ability to define more react friendly components (for use + outside cljs codebases). +- Add the ability to define lazy loading components +- Make `rumext.v2.compiler/compile-concat` public +- Improve documentation + + +## Version v2.8 + +- Export `React.lazy` as `lazy` helper +- Add `lazy-component` macro that joins react lazy with shadow-cljs + lazy loading + +## Version v2.7 + +- Update to react>=18 + + +## Version v2.6 + +- Bugfixes + + +## Version v2.5 + +- Bugfixes + + +## Version v2.4 + +- Add improve performance of internal css handling + + +## Version v2.3 + +- Minor updates +- Add with-fn macro + +## Version v2.2 + +- Add the ability to destructure js props + +## Version v2.1 + +- Make `use-id` available for react < 18. +- Add `use-equal-memo` hook. +- Add `use-debouce` hook. +- Add experimental `use-ssr-effect`. + +## Version v2.0 + +- Change version numbering: simplified. +- Add v2 namespace that compatible with React18 (still some warnings that will be addressed in next versions) + + ## Version 2022.04.19-148 - Fix htmlFor attr handling diff --git a/README.md b/README.md index 3870876..885cece 100644 --- a/README.md +++ b/README.md @@ -1,187 +1,532 @@ -# rumext # +# rumext -Simple and Decomplected UI library based on React. +Simple and Decomplected UI library based on React >= 18 focused on performance. +## Installation -## Using rumext +Add to `deps.edn`: -Add to deps.edn: +```clojure +funcool/rumext +{:git/tag "v2.21" + :git/sha "072d671" + :git/url "https://github.com/funcool/rumext.git"} +``` + +## User Guide + +Rumext is a tool to build a web UI in ClojureScript. + +It's a thin wrapper on [React](https://react.dev/) >= 18, focused on +performance and offering a Clojure-idiomatic interface. + +**API Reference**: http://funcool.github.io/rumext/latest/ + +It uses Clojure macros to achieve the same goal as [JSX +format](https://react.dev/learn/writing-markup-with-jsx) without using anything +but the plain Clojure syntax. The HTML is expressed in a format inspired +in [hiccup library](https://github.com/weavejester/hiccup), but with its own +implementation. + +HTML code is represented as nested arrays with keywords for tags and +attributes. Example: + +```clojure +[:div {:class "foobar" + :style {:background-color "red"} + :on-click some-on-click-fn} + "Hello World"] ``` -funcool/rumext {:mvn/version "2022.04.19-148"} + +Macros are smart enough to transform attribute names from `lisp-case` +to `camelCase` and renaming `:class` to `className`. So the compiled javacript +code for this fragment could be something like: + +```js +React.createElement("div", + {className: "foobar", + style: {"backgroundColor": "red"}, + onClick: someOnClickFn}, + "Hello World"); ``` -## Differences with rum +And this is what will be rendered when the app is loaded in a browser: -This project is originated as a friendly fork of -[rum](https://github.com/tonsky/rum) for a personal use but it is -evolved to be a completly independent library that right now does not -depend on it. In any case, many thanks to Tonksy for creating rum. +```html +
+ Hello World +
+``` -This is the list of the main differences: +**WARNING**: it is mainly implemented to be used in +[Penpot](https://github.com/penpot/penpot) and released as separated project +for conveniendce. Don't expect compromise for backwards compatibility beyond +what the penpot project needs. -- use function based components instead of class based components. -- a clojurescript friendly abstractions for React Hooks. -- the component body is compiled statically (never interprets at - runtime thanks to **hicada**). -- performance focused, with a goal to offer almost 0 runtime - overhead on top of React. +### Instantiating elements and custom components + +#### Passing props + +As seen above, when using the [Hiccup-like](https://github.com/weavejester/hiccup) +syntax, you can create a HTML element with a keyword like `:div`, `:span` or +`:p`. You can also specify a map of attributes, that are converted at compile +time into a Javascript object. + +**IMPORTANT**: a Javascript plain object is different from a Clojure plain map. +In ClojureScript you can handle mutable JS objects with a specific API, and +convert forth and back to Clojure maps. You can learn more about it in +[ClojureScript Unraveled](https://funcool.github.io/clojurescript-unraveled/#javascript-objects) +book. + +Rumext macros have some features to pass properties in a more convenient and +Clojure idiomatic way. For example, when using the `[:div {...}]` syntax, you +do not need to add the `#js` prefix, it's added automatically. There are also +some automatic transformations of property names: -**WARNING**: this is not intended for general use, it is mainly -implemented to be used in [penpot](https://github.com/penpot/penpot) -and released as separated project for conveniendce. Don't expect -compromise for backward compatibility. + * Names in `lisp-case` are transformed to `camelCase`. + * Reserved names like `class` are transformed to React convention, like + `className`. + * Names already in `camelCase` are passed directly without transform. + * Properties that begin with `data-` and `aria-` are also passed directly. + * Transforms are applied only to `:keyword` properties. You can also send + string properties, that are not processed anyway. +It's important to notice that this transformations are performed at compile time, +having no impact in runtime performance. -## Components -### How to define a component +#### Dynamic element names and attributes -Function components as it's name says, are defined using plain -functions. Rumext exposes a lighweigh macro over a `fn` that convert -props from js-object to cljs map (shallow) and exposes a facility for -docorate (wrap) with other higher-order components. +There are times when we'll need the element name to be chosen dynamically or +constructed at runtime; the props to be built dynamically or created as an +element from a user-defined component. -Let's see a example of how to define a component: +For this purpose, Rumext exposes a special macro: `:>`, a general-purpose +handler for passing dynamically defined props to DOM native elements or +creating elements from user-defined components. + +To define the element dynamically, just pass a variable with the name as a +first parameter of `:>`. ```clojure -(require '[rumext.alpha :as mf]) +(let [element (if something "div" "span")] + [:> element {:class "foobar" + :style {:background-color "red"} + :on-click some-on-click-fn} + "Hello World"]) +``` -(mf/defc title - [{:keys [name]}] - [:div {:class "label"} name]) +To give a dynamic map of properties, you may also give a variable as a +second parameter: + +```clojure +(let [props #js {:className "fooBar" + :style #js {:backgroundColor "red"} + :onClick some-on-click}] + [:> "div" props + "Hello World"]) ``` -If you don't want the props in cljs data structure, you can disable -the props conversion passing `::mf/wrap-props false` as metadata: +**IMPORTANT** if you define the attributes dynamically, outside the `:>` macro, +there are no automatic transformations. So you need to define the map as a +plain Javascript object with the `#js` prefix or any other way. You also need +to use `camelCase` names and remember to use `className` instead of `class`, +for example. + +There are a couple of utilities for managing dynamic attributes in a more +convenient way. + + +##### `mf/spread-props` + +Or shorter alias: `mf/spread` + +A macro that allows performing a merge between two props data structures using +the JS spread operator (`{...props1, ...props2}`). This macro also performs +name transformations if you pass a literal map as a second parameter. + +It is commonly used this way: ```clojure -(require '[goog.object :as gobj]) +(mf/defc my-label* + [{:keys [name class on-click] :rest props}] + (let [class (or class "my-label") + props (mf/spread-props props {:class class})] + [:span {:on-click on-click} + [:> :label props name]])) +``` -(mf/defc title - [props] - (let [name (gobj/get props "name")] - [:div {:class "label"} name])) +Very similar to `mf/spread-props` but without react flavored props +transformations you have the `mf/spread-object`. + +In both cases, if both arguments are symbols, no transformation +can be applied because is unknown the structure at compile time. + + +##### `mf/props` + +A helper macro to create a Javascript props object from a Clojure map, +applying name transformations. + +An example of how it can be used and combined with `mf/spread-props`: + +```clojure +(mf/defc my-label* + [{:keys [name class on-click] :rest props}] + (let [class (or class "my-label") + new-props (mf/props {:class class}) + all-props (mf/spread-props props new-props)] + [:span {:on-click on-click} + [:> :label props name]])) ``` -### First steps with hicada hiccup -You may be already familiar with hiccup syntax for defining the react -dom. The intention on this section is explain only the essential part -of it and the peculiarities of hiccada and rumext. +##### `mf/object` -Lets start with simple generic components like `:div`: +A helper macro for create javascript objects from clojure literals. It works recursiverlly. ```clojure -[:div {:class "foobar" - :style {:background-color "red"} - :on-click some-on-click-fn} - "Hello World"] +(mf/object {:a [1 2 3]}) + +;; Is analogous to +#js {:a #js [1 2 3]} +``` + + +##### `mfu/map->props` + +In some cases you will need to make props from a dynamic Clojure +object. You can use `mf/map->props` function for it, but be aware that +it makes the conversion to Javascript and the names transformations in +runtime, so it adds some overhead in each render. Consider not using +it if performance is important. + +```clojure +(require '[rumext.v2.utils :as mfu]) + +(let [clj-props {:class "my-label"} + props (mfu/map->props clj-props)] + [:> :label props name]) +``` + +##### `mfu/bean` + +A helper that allows create a proxy object from javascript object that +has the same semantics as clojure map and clojure vectors. Allows +handle clojure and javascript parameters in a transparent way. + +```clojure +(require '[rumext.v2.utils :as mfu]) + +(mf/defc my-select* + [{:keys [options] :rest props}] + (let [options (mfu/bean options) + ;; from here, options looks like a clojure vector + ;; independently if it passed as clojure vector + ;; or js array. + ] + [:select ...])) +``` + +#### Instantiating a custom component + +You can pass to `:>` macro the name of a custom component (see [below](#creating-a-react-custom-component)) +to create an instance of it: + +```clojure +(mf/defc my-label* + [{:keys [name class on-click] :rest props}] + [:span {:on-click on-click} + [:> :label props name]]) + +(mf/defc other-component* + [] + [:> my-label* {:name "foobar" :on-click some-fn}]) ``` -Until here, nothing new, looks like any hiccup template. The +### Creating a React custom component + +The `defc` macro is the basic block of a Rumext UI. It's a lightweight utility +that generates a React **function component** and adds some adaptations for it +to be more convenient to ClojureScript code, like `camelCase` conversions and +reserved name changes as explained [above](#passing-props). + +For example, this defines a React component: +```clojure +(require '[rumext.v2 :as mf]) + +(mf/defc title* + [{:keys [label-text] :as props}] + [:div {:class "title"} label-text]) +``` -As you can observe, looks very familiar. On default components the -props are transformed **recursively** at compile time to a js object -transforming all keys from kebab-case to camelCase (and rename -`:class` to `className`); so the result will look aproximatelly like -this in jsx: +The compiled javascript for this block will be similar to what would be +obtained for this JSX block: ```js -const h = React.createElement; +function title({labelText}) { + return ( +
+ {labelText} +
+ ); +} +``` + +**NOTE**: the `*` in the component name is a mandatory convention for proper +visual distinction of React components and Clojure functions. It also enables +the current defaults on how props are handled. If you don't use the `*` suffix, +the component will behave in legacy mode (see the [FAQs](#faq) below). + +The component created this way can be mounted onto the DOM: -h("div", {className: "foobar", - style: {"backgroundColor": "red"}, - onClick=someFn}, - "Hello World"); +```clojure +(ns myname.space + (:require + [goog.dom :as dom] + [rumext.v2 :as mf])) + +(def root (mf/create-root (dom/getElement "app"))) +(mf/render! root (mf/html [:> title* {:label-text "hello world"}])) ``` -TODO +Or you can use `mf/element`, but in this case you need to give the +attributes in the raw Javascript form, because this macro does not have +automatic conversions: -### Higher-Order Components +```clojure +(ns myname.space + (:require + [goog.dom :as dom] + [rumext.v2 :as mf])) -This is the way you have to extend/add additional functionality to a -function component. Rumext exposes one: +(def root (mf/create-root (dom/getElement "app"))) +(mf/render! root (mf/element title* #js {:labelText "hello world"})) +``` -- `mf/memo`: analogous to `React.memo`, adds memoization to the - component based on props comparison. +### Reading component props & destructuring -In order to use the high-order components, you need wrap the component manually -or passing it as a special property in the metadata: +When React instantiates a function component, it passes a `props` parameter +that is a map of the names and values of the attributes defined in the calling +point. + +Normally, Javascript objects cannot be destructured. But the `defc` macro +implements a destructuring functionality, that is similar to what you can do +with Clojure maps, but with small differences and convenient enhancements for +making working with React props and idioms easy, like `camelCase` conversions +as explained [above](#passing-props). ```clojure -(mf/defc title - {::mf/wrap [mf/wrap-memo]} - [props] - [:div {:class "label"} (:name props)]) +(mf/defc title* + [{:keys [title-name] :as props}] + (assert (object? props) "expected object") + (assert (string? title-name) "expected string") + [:label {:class "label"} title-name]) +``` + +If the component is called via the `[:>` macro (explained [above](#dynamic-element-names-and-attributes)), +there will be two compile-time conversion, one when calling and another one when +destructuring. In the Clojure code all names will be `lisp-case`, but if you +inspect the generated Javascript code, you will see names in `camelCase`. + +#### Default values + +Also like usual destructuring, you can give default values to properties by +using the `:or` construct: + +```clojure +(mf/defc color-input* + [{:keys [value select-on-focus] :or {select-on-focus true} :as props}] + ...) ``` -By default `identical?` predicate is used for compare props; this is -how you can pass a custom compare function: +#### Rest props + +An additional idiom (specific to the Rumext component macro and not available +in standard Clojure destructuring) is the ability to obtain an object with all +non-destructured props with the `:rest` construct. This allows to extract the +props that the component has control of and leave the rest in an object that +can be passed as-is to the next element. ```clojure -(mf/defc title - {::mf/wrap [#(mf/wrap-memo % =)]} +(mf/defc title* + [{:keys [name] :rest props}] + (assert (object? props) "expected object") + (assert (nil? (unchecked-get props "name")) "no name in props") + + ;; See below for the meaning of `:>` + [:> :label props name]) +``` + +#### Reading props without destructuring + +Of course the destructure is optional. You can receive the complete `props` +argument and read the properties later. But in this case you will not have +the automatic conversions: + +```clojure +(mf/defc color-input* [props] - [:div {:class "label"} (:name props)]) + (let [value (unchecked-get props "value") + on-change (unchecked-get props "onChange") + on-blur (unchecked-get props "onBlur") + on-focus (unchecked-get props "onFocus") + select-on-focus? (or (unchecked-get props "selectOnFocus") true) + class (or (unchecked-get props "className") "color-input") ``` -If you want create a own high-order component you can use `mf/fnc` macro: +The recommended way of reading `props` javascript objects is by using the +Clojurescript core function `unchecked-get`. This is directly translated to +Javascript `props["propName"]`. As Rumext is performance oriented, this is the +most efficient way of reading props for the general case. Other methods like +`obj/get` in Google Closure Library add extra safety checks, but in this case +it's not necessary since the `props` attribute is guaranteed by React to have a +value, although it can be an empty object. + +#### Forwarding references + +In React there is a mechanism to set a reference to the rendered DOM element, if +you need to manipulate it later. Also it's possible that a component may receive +this reference and gives it to a inner element. This is called "forward referencing" +and to do it in Rumext, you need to add the `forward-ref` metadata. Then, the +reference will come in a second argument to the `defc` macro: ```clojure -(defn some-factory - [component param] - (mf/fnc myhighordercomponent - {::mf/wrap-props false} - [props] - [:section - [:> component props]])) +(mf/defc wrapped-input* + {::mf/forward-ref true} + [props ref] + (let [...] + [:input {:style {...} + :ref ref + ...}])) +``` + +In React 19 this will not be necessary, since you will be able to pass the ref +directly inside `props`. But Rumext currently only support React 18. + +### Props Checking + +The Rumext library comes with two approaches for checking props: +**simple** and **malli**. + +Let's start with the **simple**, which consists of simple existence checks or +plain predicate checking. For this, we have the `mf/expect` macro that receives +a Clojure set and throws an exception if any of the props in the set has not +been given to the component: + +```clojure +(mf/defc button* + {::mf/expect #{:name :on-click}} + [{:keys [name on-click]}] + [:button {:on-click on-click} name]) +``` + +The prop names obey the same rules as the destructuring so you should use the +same names. + +Sometimes a simple existence check is not enough; for those cases, you can give +`mf/expect` a map where keys are props and values are predicates: + +```clojure +(mf/defc button* + {::mf/expect {:name string? + :on-click fn?}} + [{:keys [name on-click]}] + [:button {:on-click on-click} name]) ``` +If that is not enough, you can use `mf/schema` macro that supports +**[malli](https://github.com/metosin/malli)** schemas as a validation +mechanism for props: -### Hooks (React Hooks) +```clojure +(def ^:private schema:props + [:map {:title "button:props"} + [:name string?] + [:class {:optional true} string?] + [:on-click fn?]]) + +(mf/defc button* + {::mf/schema schema:props} + [{:keys [name on-click]}] + [:button {:on-click on-click} name]) +``` + +**IMPORTANT**: The props checking obeys the `:elide-asserts` compiler +option and by default, they will be removed in production builds if +the configuration value is not changed explicitly. + +### Hooks + +You can use React hooks as is, as they are exposed by Rumext as +`mf/xxx` wrapper functions. Additionaly, Rumext offers several +specific hooks that adapt React ones to have a more Clojure idiomatic +interface. + +You can use both one and the other interchangeably, depending on which +type of API you feel most comfortable with. The React hooks are exposed +as they are in React, with the function name in `camelCase`, and the +Rumext hooks use the `lisp-case` syntax. -React hooks is a basic primitive that React exposes for add state and -side-effects to functional components. Rumext exposes right now only -three hooks with a ClojureScript based api. +Only a subset of available hooks is documented here; please refer to +the [React API reference +documentation](https://react.dev/reference/react/hooks) for detailed +information about available hooks. +#### `use-state` -#### use-state (React.useState) +This is analogous to the `React.useState`. It offers the same +functionality but uses the ClojureScript atom interface. -Hook used for maintain a local state and in functional -components. Calling `mf/use-state` returns an atom-like object that -will deref to the current value and you can call `swap!` and `reset!` -on it for modify its state. +Calling `mf/use-state` returns an atom-like object that will deref to +the current value, and you can call `swap!` and `reset!` on it to +modify its state. The returned object always has a stable reference +(no changes between rerenders). Any mutation will schedule the component to be rerendered. ```clojure -(require '[rumext.alpha as mf]) +(require '[rumext.v2 as mf]) -(mf/defc local-state +(mf/defc local-state* [props] - (let [local (mf/use-state 0)] - [:div {:on-click #(swap! local inc)} - [:span "Clicks: " @local]])) + (let [clicks (mf/use-state 0)] + [:div {:on-click #(swap! clicks inc)} + [:span "Clicks: " @clicks]])) +``` + +This is functionally equivalent to using the React hook directly: -(mf/mount (mf/element local-state) js/document.body) +```clojure +(mf/defc local-state* + [props] + (let [[counter update-counter] (mf/useState 0)] + [:div {:on-click (partial update-counter #(inc %))} + [:span "Clicks: " counter]])) ``` -#### use-var (React.useRef) +#### `use-var` + +In the same way as `use-state` returns an atom-like object. The unique +difference is that updating the ref value does not schedule the +component to rerender. Under the hood, it uses the `useRef` hook. -In the same way as `use-state` returns an atom like object. The unique -difference is that updating the ref value does not schedules the -component to rerender. +**DEPRECATED:** should not be used +#### `use-effect` -#### use-effect (React.useEffect) +Analogous to the `React.useEffect` hook with a minimal call convention +change (the order of arguments is inverted). -This is a primitive that allows incorporate probably efectful code +This is a primitive that allows incorporating probably effectful code into a functional component: ```clojure -(mf/defc local-timer +(mf/defc local-timer* [props] (let [local (mf/use-state 0)] (mf/use-effect @@ -189,19 +534,18 @@ into a functional component: (let [sem (js/setInterval #(swap! local inc) 1000)] #(js/clearInterval sem)))) [:div "Counter: " @local])) - -(mf/mount (mf/element local-state) js/document.body) ``` -The `use-effect` is a two arity function. If you pass a single -callback function it acts like there are no dependencies, so the -callback will be executed once per component (analgous to `didMount` +The `use-effect` is a two-arity function. If you pass a single +callback function, it acts as though there are no dependencies, so the +callback will be executed once per component (analogous to `didMount` and `willUnmount`). -If you want to pass dependencies you have two ways: +If you want to pass dependencies, you have two ways: -- passing an js array -- using `rumext.alpha/deps` helper +- passing a JS array as a first argument (like in React but with + inverted order). +- using the `rumext.v2/deps` helper: ```clojure (mf/use-effect @@ -210,32 +554,68 @@ If you want to pass dependencies you have two ways: ``` And finally, if you want to execute it on each render, pass `nil` as -deps (much in the same way as raw useEffect works. +deps (much in the same way as raw `useEffect` works). + +For convenience, there is an `mf/with-effect` macro that drops one +level of indentation: + +```clojure +(mf/defc local-timer* + [props] + (let [local (mf/use-state 0)] + (mf/with-effect [] + (let [sem (js/setInterval #(swap! local inc) 1000)] + #(js/clearInterval sem))) + [:div "Counter: " @local])) +``` + +Here, the deps must be passed as elements within the vector (the first +argument). +Obviously, you can also use the React hook directly via `mf/useEffect`. -#### use-memo (React.useMemo) +#### `use-memo` -The purpose of this hook is return a memoized value. +In the same line as the `use-effect`, this hook is analogous to the +React `useMemo` hook with the order of arguments inverted. + +The purpose of this hook is to return a memoized value. Example: ```clojure -(mf/defc sample-component +(mf/defc sample-component* [{:keys [x]}] (let [v (mf/use-memo (mf/deps x) #(pow x 10))] - [:span "Value is:" v])) + [:span "Value is: " v])) ``` On each render, while `x` has the same value, the `v` only will be calculated once. -There is also the `rumext.alpha/use-callback` for a specific use -cases. +This also can be expressed with the `rumext.v2/with-memo` macro that +removes a level of indentation: +```clojure +(mf/defc sample-component* + [{:keys [x]}] + (let [v (mf/with-memo [x] + (pow x 10))] + [:span "Value is: " v])) +``` + +#### `use-fn` + +Is a special case of `use-memo`in that the memoized value is a +function definition. -#### deref +An alias for `use-callback`, that is a wrapper on `React.useCallback`. -A custom hook that adds ractivity to atom changes to the component. +#### `deref` + +A Rumext custom hook that adds reactivity to atom changes to the +component. Calling `mf/deref` returns the same value as the Clojure +`deref`, but also sets a component rerender when the value changes. Example: @@ -243,28 +623,116 @@ Example: (def clock (atom (.getTime (js/Date.)))) (js/setInterval #(reset! clock (.getTime (js/Date.))) 160) -(mf/defc timer +(mf/defc timer* [props] (let [ts (mf/deref clock)] - [:div "Timer (deref)" ": " + [:div "Timer (deref): " [:span ts]])) ``` +Internally, it uses the `react.useSyncExternalStore` API together with +the ability of atom to watch it. + +### Higher-Order Components + +React allows to create a component that adapts or wraps another component +to extend it and add additional functionality. Rumext includes a convenient +mechanism for doing it: the `::mf/wrap` metadata. + +Currently Rumext exposes one such component: + +- `mf/memo`: analogous to `React.memo`, adds memoization to the + component based on props comparison. This allows to completely + avoid execution to the component function if props have not changed. + +```clojure +(mf/defc title* + {::mf/wrap [mf/memo]} + [{:keys [name]}] + [:div {:class "label"} name]) +``` + +By default, the `identical?` predicate is used to compare props; you +can pass a custom comparator function as a second argument: + +```clojure +(mf/defc title* + {::mf/wrap [#(mf/memo % =)]} + [{:keys [name]}] + [:div {:class "label"} name]) +``` + +For more convenience, Rumext has a special metadata `::mf/memo` that +facilitates the general case for component props memoization. If you +pass `true`, it will behave the same way as `::mf/wrap [mf/memo]` or +`React.memo(Component)`. You also can pass a set of fields; in this +case, it will create a specific function for testing the equality of +that set of props. + +If you want to create your own higher-order component, you can use the +`mf/fnc` macro: + +```clojure +(defn some-factory + [component param] + (mf/fnc my-high-order-component* + [props] + [:section + [:> component props]])) +``` + +### FAQ + +#### Differences with RUM + +This project was originated as a friendly fork of +[rum](https://github.com/tonsky/rum) for a personal use but it later +evolved to be a completly independent library that right now does not +depend on it and probably no longer preserves any of the original +code. In any case, many thanks to Tonksy for creating rum. + +This is the list of the main differences: + +- use function based components instead of class based components. +- a clojurescript friendly abstractions for React Hooks. +- the component body is compiled statically (never interprets at + runtime thanks to **hicada**). +- performance focused, with a goal to offer almost 0 runtime + overhead on top of React. + + +#### Why the import alias is `mf` in the examples? + +The usual convention of importing RUM project was to use `rum/defc` or +`m/defc`. For Rumext the most straightforward abbreviation would have been +`mx/defc`. But that preffix was already use for something else. So finally we +choose `mf/defc`. But this is not mandatory, it's only a convention we follow +in this manual and in Penpot. + -#### Raw Hooks +#### What is the legacy mode? -In some circumstances you will want access to the raw react hooks -functions. For this purpose, rumext exposes the following functions: -`useState`, `useRef`, `useMemo`, `useCallback`, `useLayoutEffect` and -`useEffect`. +In earlier versions of Rumext, components had a default behavior of +automatically converting the `props` Javascript object coming from +React to a Clojure object, so it could be read by normal destructuring +or any other way of reading objects. -#### Other undocumented stuff +Additionally you could use `:&` handler instead of `:>` to give a +Clojure object that was converted into Javascript for passing it to +React. -- Error boundaries: `mf/catch` high-order component. -- Raw `React.memo`: `mf/memo'`. -- Create element: `mf/element` and `mf/create-element`. +But both kind of transformations were done in runtime, thus adding +the conversion overhead to each render of the compoennt. Since Rumex +is optimized for performance, this behavior is now deprecated. With +the macro destructuring and other utilities explained above, you can +do argument passing almost so conveniently, but with all changes done +in compile time. +Currently, components whose name does not use `*` as a suffix behave +in legacy mode. You can activate the new behavior by adding the +`::mf/props :obj` metadata, but all this is considered deprecated now. +All new components should use `*` in the name. -## License ## +## License -Licensed under Eclipse Public License (see [LICENSE](LICENSE)). +Licensed under MPL-2.0 (see [LICENSE](LICENSE) file on the root of the repository) diff --git a/build.clj b/build.clj index c774ac7..49c9b1a 100644 --- a/build.clj +++ b/build.clj @@ -3,7 +3,7 @@ (:require [clojure.tools.build.api :as b])) (def lib 'funcool/rumext) -(def version (format "2022.04.19-%s" (b/git-count-revs nil))) +(def version (format "v2-%s" (b/git-count-revs nil))) (def class-dir "target/classes") (def basis (b/create-basis {:project "deps.edn"})) (def jar-file (format "target/%s-%s.jar" (name lib) version)) diff --git a/deps.edn b/deps.edn index e35d15d..840f4f0 100644 --- a/deps.edn +++ b/deps.edn @@ -1,4 +1,6 @@ -{:deps {} +{:deps {metosin/malli {:mvn/version "0.16.0"} + funcool/cuerdas {:mvn/version "2023.11.09-407"} + cljs-bean/cljs-bean {:mvn/version "1.9.0"}} :paths ["src"] :aliases {:dev @@ -9,12 +11,18 @@ thheller/shadow-cljs {:mvn/version "RELEASE"} org.clojure/tools.namespace {:mvn/version "RELEASE"} org.clojure/test.check {:mvn/version "RELEASE"} - ;; org.clojure/tools.deps.alpha {:mvn/version "RELEASE"} - org.clojure/clojure {:mvn/version "1.10.3"} + org.clojure/clojure {:mvn/version "RELEASE"} }} + :codox + {:extra-deps + {codox/codox {:mvn/version "RELEASE"} + org.clojure/tools.reader {:mvn/version "RELEASE"} + codox-theme-rdash/codox-theme-rdash {:mvn/version "RELEASE"}}} + :shadow-cljs - {:main-opts ["-m" "shadow.cljs.devtools.cli"]} + {:main-opts ["-m" "shadow.cljs.devtools.cli"] + :jvm-opts ["--sun-misc-unsafe-memory-access=allow"]} :repl {:main-opts ["-m" "rebel-readline.main"]} @@ -25,5 +33,5 @@ :main-opts ["-m" "antq.core"]} :build - {:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.1" :git/sha "7d40500"}} + {:extra-deps {io.github.clojure/tools.build {:git/tag "v0.10.3" :git/sha "15ead66"}} :ns-default build}}} diff --git a/doc.clj b/doc.clj new file mode 100644 index 0000000..4a22bcd --- /dev/null +++ b/doc.clj @@ -0,0 +1,11 @@ +(require '[codox.main :as codox]) + +(codox/generate-docs + {:output-path "doc/dist/latest" + :metadata {:doc/format :markdown} + :language :clojurescript + :name "funcool/rumext" + :themes [:rdash] + :source-paths ["src"] + :namespaces [#"^rumext\."] + :source-uri "https://github.com/funcool/rumext/blob/v2/{filepath}#L{line}"}) diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..e476300 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,9 @@ +all: doc + +doc: + mkdir -p dist/latest/ + cd ..; clojure -A:dev:codox -M doc.clj; + +github: doc + ghp-import -m "Generate documentation" -b gh-pages dist/ + git push origin gh-pages diff --git a/doc/notes.txt b/doc/notes.txt new file mode 100644 index 0000000..e1a645d --- /dev/null +++ b/doc/notes.txt @@ -0,0 +1,5 @@ +Build browserified bundle: +./node_modules/browserify/bin/cmd.js -s Rx -e dist/cjs/Rx.js -o rx.js + +Minified bundle: +./node_modules/uglify-js/bin/uglifyjs rx.js -m -o rx.min.js diff --git a/examples/rumext/examples/binary_clock.cljs b/examples/rumext/examples/binary_clock.cljs index c158a96..dc3cf26 100644 --- a/examples/rumext/examples/binary_clock.cljs +++ b/examples/rumext/examples/binary_clock.cljs @@ -1,24 +1,27 @@ (ns rumext.examples.binary-clock - (:require [goog.dom :as dom] - [rumext.alpha :as mf] - [rumext.examples.util :as util])) + (:require + [goog.dom :as dom] + [rumext.v2 :as mf] + [rumext.examples.util :as util])) (def *bclock-renders (atom 0)) -(mf/defc render-count +(mf/defc render-count* [props] (let [renders (mf/deref *bclock-renders)] [:div.stats "Renders: " renders])) -(mf/defc bit - [{:keys [n b] :as props}] - (mf/use-effect (mf/deps n b) #(swap! *bclock-renders inc)) +(mf/defc bit* + [{:keys [n b]}] + (mf/with-effect [n b] + (swap! *bclock-renders inc)) + (let [color (mf/deref util/*color)] (if (bit-test n b) [:td.bclock-bit {:style {:background-color color}}] [:td.bclock-bit {}]))) -(mf/defc binary-clock +(mf/defc binary-clock* [] (let [ts (mf/deref util/*clock) msec (mod ts 1000) @@ -37,41 +40,41 @@ [:table.bclock [:tbody [:tr - [:td] [:& bit {:n hl :b 3}] [:th] - [:td] [:& bit {:n ml :b 3}] [:th] - [:td] [:& bit {:n sl :b 3}] [:th] - [:& bit {:n msh :b 3}] - [:& bit {:n msm :b 3}] - [:& bit {:n msl :b 3}]] + [:td] [:> bit* {:n hl :b 3}] [:th] + [:td] [:> bit* {:n ml :b 3}] [:th] + [:td] [:> bit* {:n sl :b 3}] [:th] + [:> bit* {:n msh :b 3}] + [:> bit* {:n msm :b 3}] + [:> bit* {:n msl :b 3}]] [:tr - [:td] [:& bit {:n hl :b 2}] [:th] - [:& bit {:n mh :b 2}] - [:& bit {:n ml :b 2}] [:th] - [:& bit {:n sh :b 2}] - [:& bit {:n sl :b 2}] [:th] - [:& bit {:n msh :b 2}] - [:& bit {:n msm :b 2}] - [:& bit {:n msl :b 2}]] + [:td] [:> bit* {:n hl :b 2}] [:th] + [:> bit* {:n mh :b 2}] + [:> bit* {:n ml :b 2}] [:th] + [:> bit* {:n sh :b 2}] + [:> bit* {:n sl :b 2}] [:th] + [:> bit* {:n msh :b 2}] + [:> bit* {:n msm :b 2}] + [:> bit* {:n msl :b 2}]] [:tr - [:& bit {:n hh :b 1}] - [:& bit {:n hl :b 1}] [:th] - [:& bit {:n mh :b 1}] - [:& bit {:n ml :b 1}] [:th] - [:& bit {:n sh :b 1}] - [:& bit {:n sl :b 1}] [:th] - [:& bit {:n msh :b 1}] - [:& bit {:n msm :b 1}] - [:& bit {:n msl :b 1}]] + [:> bit* {:n hh :b 1}] + [:> bit* {:n hl :b 1}] [:th] + [:> bit* {:n mh :b 1}] + [:> bit* {:n ml :b 1}] [:th] + [:> bit* {:n sh :b 1}] + [:> bit* {:n sl :b 1}] [:th] + [:> bit* {:n msh :b 1}] + [:> bit* {:n msm :b 1}] + [:> bit* {:n msl :b 1}]] [:tr - [:& bit {:n hh :b 0}] - [:& bit {:n hl :b 0}] [:th] - [:& bit {:n mh :b 0}] - [:& bit {:n ml :b 0}] [:th] - [:& bit {:n sh :b 0}] - [:& bit {:n sl :b 0}] [:th] - [:& bit {:n msh :b 0}] - [:& bit {:n msm :b 0}] - [:& bit {:n msl :b 0}]] + [:> bit* {:n hh :b 0}] + [:> bit* {:n hl :b 0}] [:th] + [:> bit* {:n mh :b 0}] + [:> bit* {:n ml :b 0}] [:th] + [:> bit* {:n sh :b 0}] + [:> bit* {:n sl :b 0}] [:th] + [:> bit* {:n msh :b 0}] + [:> bit* {:n msm :b 0}] + [:> bit* {:n msl :b 0}]] [:tr [:th hh] [:th hl] @@ -87,8 +90,11 @@ [:th msl]] [:tr [:th {:col-span 8} - [:& render-count]]]]])) + [:> render-count* {}]]]]])) + +(defonce root + (mf/create-root (dom/getElement "binary-clock"))) -(defn mount! [] - (mf/mount (mf/element binary-clock) (dom/getElement "binary-clock"))) +(defn ^:after-load mount! [] + (mf/render! root (mf/element binary-clock*))) diff --git a/examples/rumext/examples/board.cljs b/examples/rumext/examples/board.cljs index da73d13..5b06651 100644 --- a/examples/rumext/examples/board.cljs +++ b/examples/rumext/examples/board.cljs @@ -1,7 +1,7 @@ (ns rumext.examples.board (:require [goog.dom :as dom] - [rumext.alpha :as mf] + [rumext.v2 :as mf] [rumext.examples.util :as util] [okulary.core :as l])) @@ -16,8 +16,9 @@ (l/derived (l/in [y x]) board)) cell (mf/deref ref) color (mf/deref util/*color)] - [:div.art-cell - {:style {:background-color (when cell color)} + [:div + {:class "art-cell" + :style {:background-color (when cell color)} :on-mouse-over (fn [_] (swap! board update-in [y x] not) nil)}])) (mf/defc board-reactive @@ -26,13 +27,15 @@ (for [y (range 0 util/board-height)] [:div.art-row {:key y} (for [x (range 0 util/board-width)] - ;; this is how one can specify React key for component - [:& cell {:key x :x x :y y}])])]) + (let [props #js {:key x :x x :y y}] + ;; this is how one can specify React key for component + [:& cell ^js props]))])]) -(defn mount! [] - (mf/mount (mf/element board-reactive) - (dom/getElement "board")) +(defonce root + (mf/create-root (dom/getElement "board"))) + +(defn ^:after-load mount! [] + (mf/render! root (mf/element board-reactive)) (js/setTimeout (fn [] - (mf/mount (mf/element board-reactive) - (dom/getElement "board"))) + (mf/render! root (mf/element board-reactive))) 2000)) diff --git a/examples/rumext/examples/controls.cljs b/examples/rumext/examples/controls.cljs index 5993b4b..050af9f 100644 --- a/examples/rumext/examples/controls.cljs +++ b/examples/rumext/examples/controls.cljs @@ -1,10 +1,10 @@ (ns rumext.examples.controls - (:require [goog.dom :as dom] - [rumext.alpha :as mf] - [rumext.examples.util :as util])) + (:require + [goog.dom :as dom] + [rumext.v2 :as mf] + [rumext.examples.util :as util])) -;; generic “atom editor” component -(mf/defc input +(mf/defc input* [{:keys [color] :as props}] (let [value (mf/deref color)] [:input {:type "text" @@ -13,25 +13,28 @@ :on-change #(reset! color (.. % -target -value))}])) ;; Raw top-level component, everything interesting is happening inside -(mf/defc controls +(mf/defc controls* [props] [:dl [:dt "Color: "] [:dd - [:& input {:color util/*color}]] + [:> input* {:color util/*color}]] ;; Binding another component to the same atom will keep 2 input boxes in sync [:dt "Clone: "] [:dd - (mf/element input {:color util/*color})] + (mf/jsx input* #js {:color util/*color})] [:dt "Color: "] [:dd {} (util/watches-count {:iref util/*color}) " watches"] [:dt "Tick: "] - [:dd [:& input {:color util/*speed}] " ms"] + [:dd [:> input* {:color util/*speed}] " ms"] [:dt "Time:"] [:dd {} (util/watches-count {:iref util/*clock}) " watches"] ]) -(defn mount! [] - (mf/mount (mf/element controls) (dom/getElement "controls"))) +(defonce root + (mf/create-root (dom/getElement "controls"))) + +(defn ^:after-load mount! [] + (mf/render! root (mf/element controls*))) diff --git a/examples/rumext/examples/core.cljs b/examples/rumext/examples/core.cljs index e8fb7ba..cb4ed71 100644 --- a/examples/rumext/examples/core.cljs +++ b/examples/rumext/examples/core.cljs @@ -1,24 +1,24 @@ (ns rumext.examples.core (:require - [rumext.alpha :as mf] [rumext.examples.binary-clock :as binary-clock] [rumext.examples.timer-reactive :as timer-reactive] [rumext.examples.local-state :as local-state] [rumext.examples.refs :as refs] [rumext.examples.controls :as controls] + [rumext.examples.portals :as portals] [rumext.examples.board :as board] ;; [rumext.examples.errors :as errors] )) (enable-console-print!) +(local-state/mount!) (binary-clock/mount!) (timer-reactive/mount!) -(local-state/mount!) - (refs/mount!) (controls/mount!) (board/mount!) +(portals/mount!) (defn main [& args] diff --git a/examples/rumext/examples/local_state.cljs b/examples/rumext/examples/local_state.cljs index ce40315..ebd7fa2 100644 --- a/examples/rumext/examples/local_state.cljs +++ b/examples/rumext/examples/local_state.cljs @@ -1,43 +1,53 @@ - (ns rumext.examples.local-state - (:require [goog.dom :as dom] - [rumext.alpha :as mf] - [rumext.examples.util :as util])) - -;; (mf/defc label -;; {:wrap [mf/wrap-memo]} -;; [{:keys [state] :as props}] -;; ;; (prn "label" props) -;; (let [{:keys [title n]} state] -;; [:div -;; [:span title ": " n]])) - -(def label - (mf/fnc label - {::mf/wrap [mf/memo]} - [{:keys [state] :as props}] - - (let [{:keys [title n]} state] - [:* - [:div - [:span title ": " n]]]))) +(ns rumext.examples.local-state + (:require + [goog.dom :as dom] + [malli.core :as m] + [rumext.v2 :as mf] + [rumext.v2.util :as mfu] + [rumext.examples.util :as util])) +(def schema:label + [:map {:title "label:props"} + [:on-click {:optional true} fn?] + [:my-id {:optional true} :keyword] + [:title :string] + [:n number?]]) + +(mf/defc label* + {::mf/memo true + ::mf/schema schema:label} + [{:keys [class title n my-id] :as props :rest others}] + (let [ref (mf/use-var nil) + props (mf/spread-props others {:class (or class "my-label")})] + + (mf/with-effect [] + (reset! ref 1)) + + [:> :div props + [:span title ": " n]])) (mf/defc local-state "test docstring" - {::mf/wrap [mf/memo]} - [{:keys [title] :as props}] - (let [local (mf/use-state {:counter1 {:title "Counter 1" - :n 0} - :counter2 {:title "Counter 2" - :n 0}})] - [:section {:class "counters"} + {::mf/memo true + ::mf/props :obj} + [{:keys [title]}] + (let [local (mf/use-state + #(-> {:counter1 {:title "Counter 1" + :n 0} + :counter2 {:title "Counter 2" + :n 0}}))] + + [:section {:class "counters" :style {:-webkit-border-radius "10px"}} [:hr] - [:& label {:state (:counter1 @local)}] - [:& label {:state (:counter2 @local)}] + (let [{:keys [title n]} (:counter1 @local)] + [:> label* {:n n :my-id "should-be-keyword" :title title :data-foobar 1 :on-click identity :id "foobar"}]) + (let [{:keys [title n]} (:counter2 @local)] + [:> label* {:title title :n n :on-click identity}]) [:button {:on-click #(swap! local update-in [:counter1 :n] inc)} "Increment Counter 1"] [:button {:on-click #(swap! local update-in [:counter2 :n] inc)} "Increment Counter 2"]])) +(defonce root + (mf/create-root (dom/getElement "local-state-1"))) -(defn mount! [] - (mf/mount (mf/element local-state {:title "Clicks count"}) - (dom/getElement "local-state-1"))) +(defn ^:after-load mount! [] + (mf/render! root (mf/element local-state #js {:title "Clicks count"}))) diff --git a/examples/rumext/examples/portals.cljs b/examples/rumext/examples/portals.cljs index 00f264e..814c555 100644 --- a/examples/rumext/examples/portals.cljs +++ b/examples/rumext/examples/portals.cljs @@ -1,22 +1,26 @@ (ns rumext.examples.portals - #_(:require [rumext.core :as mx] - [rumext.examples.util :as util])) + (:require + [rumext.v2 :as mf] + [goog.dom :as dom])) -;; (mx/defc portal -;; [*clicks] -;; [:div {:on-click (fn [_] (swap! *clicks inc)) -;; :style { :user-select "none", :cursor "pointer" }} -;; "[ PORTAL Clicks: " @*clicks " ]"]) +(mf/defc portal* + [{:keys [state]}] + [:div {:on-click (fn [_] (swap! state inc)) + :style { :user-select "none", :cursor "pointer" }} + "[ PORTAL Clicks: " @state " ]"]) +(mf/defc portals* + [] + (let [state (mf/use-state 0)] + [:div {:on-click (fn [_] (swap! state inc)) + :style { :user-select "none", :cursor "pointer" }} + "[ ROOT Clicks: " @state " ]" + (mf/portal + (mf/html [:> portal* {:state state}]) + (dom/getElement "portal-off-root"))])) -;; (mx/defcs portals -;; {:mixins [(mx/local 0 ::*clicks)]} -;; [{*clicks ::*clicks}] -;; [:div {:on-click (fn [_] (swap! *clicks inc)) -;; :style { :user-select "none", :cursor "pointer" }} -;; "[ ROOT Clicks: " @*clicks " ]" -;; (mx/portal (portal *clicks) (util/el "portal-off-root"))]) +(defonce root + (mf/create-root (dom/getElement "portals"))) - -;; (defn mount! [el] -;; (mx/mount (portals) el)) +(defn ^:after-load mount! [] + (mf/render! root (mf/element portals*))) diff --git a/examples/rumext/examples/refs.cljs b/examples/rumext/examples/refs.cljs index 5fcec27..8275f00 100644 --- a/examples/rumext/examples/refs.cljs +++ b/examples/rumext/examples/refs.cljs @@ -1,6 +1,7 @@ (ns rumext.examples.refs - (:require [goog.dom :as dom] - [rumext.alpha :as mf])) + (:require + [goog.dom :as dom] + [rumext.v2 :as mf])) (mf/defc textarea [props] @@ -29,5 +30,8 @@ [:div [:& textarea]]) -(defn mount! [] - (mf/mount (mf/element refs) (dom/getElement "refs"))) +(defonce root + (mf/create-root (dom/getElement "refs"))) + +(defn ^:after-load mount! [] + (mf/render! root (mf/element refs))) diff --git a/examples/rumext/examples/timer_reactive.cljs b/examples/rumext/examples/timer_reactive.cljs index 1b96c89..ea0c2e8 100644 --- a/examples/rumext/examples/timer_reactive.cljs +++ b/examples/rumext/examples/timer_reactive.cljs @@ -1,7 +1,8 @@ (ns rumext.examples.timer-reactive - (:require [goog.dom :as dom] - [rumext.alpha :as mf] - [rumext.examples.util :as util])) + (:require + [goog.dom :as dom] + [rumext.v2 :as mf] + [rumext.examples.util :as util])) (defonce components (atom {})) @@ -21,13 +22,15 @@ [:span {:style {:color @util/*color}} (util/format-time ts)]]) -(defn mount! [] - (mf/mount (mf/element timer1) - (dom/getElement "timer1")) - (mf/mount (mf/element timer2 {:ts @util/*clock}) - (dom/getElement "timer2")) +(defonce root1 + (mf/create-root (dom/getElement "timer1"))) +(defonce root2 + (mf/create-root (dom/getElement "timer2"))) + +(defn ^:after-load mount! [] + (mf/render! root1 (mf/jsx timer1 {})) + (mf/render! root2 (mf/jsx timer2 #js {:ts @util/*clock})) (add-watch util/*clock :timer-static (fn [_ _ _ ts] - (mf/mount (mf/element timer2 {:ts ts}) - (dom/getElement "timer2"))))) + (mf/render! root2 (mf/jsx timer2 #js {:ts ts}))))) diff --git a/examples/rumext/examples/util.cljs b/examples/rumext/examples/util.cljs index 278b23a..8245ea0 100644 --- a/examples/rumext/examples/util.cljs +++ b/examples/rumext/examples/util.cljs @@ -1,6 +1,6 @@ (ns rumext.examples.util (:require - [rumext.alpha :as mf] + [rumext.v2 :as mf] [goog.dom :as dom] [okulary.core :as l])) @@ -23,14 +23,12 @@ (mf/defc watches-count [{:keys [iref] :as props}] (let [state (mf/use-state 0)] - (mf/use-effect - (mf/deps iref) - (fn [] - (let [sem (js/setInterval #(swap! state inc) 1000)] - #(do - (js/clearInterval sem))))) - - [:span (.-size (.-watches iref))])) + (mf/with-effect [iref] + (let [sem (js/setInterval #(swap! state inc) 1000)] + #(do + (js/clearInterval sem)))) + + [:span (.-size (.-watches ^js iref))])) ;; Generic board utils diff --git a/package.json b/package.json index dc5f3f9..87f6825 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,12 @@ "version": "1.0.0", "description": "Simple and Decomplected UI library based on React.", "dependencies": { - "react": "17.0.1", - "react-dom": "17.0.1", - "shadow-cljs": "2.11.8" + "process": "^0.11.10", + "react": "19.1.0", + "react-dom": "19.1.0" }, - "devDependencies": {}, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "watch": "clojure -M:dev:shadow-cljs watch examples" }, "repository": { "type": "git", @@ -20,5 +19,6 @@ "bugs": { "url": "https://github.com/funcool/rumext/issues" }, - "homepage": "https://github.com/funcool/rumext#readme" + "homepage": "https://github.com/funcool/rumext#readme", + "packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538" } diff --git a/pom.xml b/pom.xml index c4281b4..ffd483b 100644 --- a/pom.xml +++ b/pom.xml @@ -36,12 +36,22 @@ org.clojure clojure - 1.10.3 + 1.11.4 - hicada - hicada - 0.1.9 + cljs-bean + cljs-bean + 1.9.0 + + + funcool + cuerdas + 2023.11.09-407 + + + metosin + malli + 0.16.0 diff --git a/shadow-cljs.edn b/shadow-cljs.edn index 7ba9f55..b278e97 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -1,5 +1,5 @@ {:deps {:aliases [:dev]} - :dev-http {9500 "classpath:public"} + :dev-http {9500 ["public" "classpath:public"]} :builds {:examples @@ -7,8 +7,15 @@ :output-dir "target/public/js" :asset-path "/js" :modules {:main {:entries [rumext.examples.core]}} - :compiler-options {:output-feature-set :es8} - :release {:compiler-options {:pseudo-names true - :pretty-print true}} + :compiler-options {:output-feature-set :es-next} + + :js-options + {:entry-keys ["module" "browser" "main"] + :export-conditions ["module" "import", "browser" "require" "default"]} + + :release + {:compiler-options + {:pseudo-names false + :pretty-print true}} }}} diff --git a/src/rumext/alpha.clj b/src/rumext/alpha.clj deleted file mode 100644 index 742fb08..0000000 --- a/src/rumext/alpha.clj +++ /dev/null @@ -1,120 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) Andrey Antukh - -(ns rumext.alpha - (:require - [rumext.compiler :as hc])) - -(defmacro html - [body] - (hc/compile body)) - -(defn parse-defc - [args] - (loop [r {} - s 0 - v (first args) - n (rest args)] - (case s - 0 (if (symbol? v) - (recur (assoc r :cname v) (inc s) (first n) (rest n)) - (recur (assoc r :cname (gensym "anonymous-")) (inc s) v n)) - 1 (if (string? v) - (recur (assoc r :doc v) (inc s) (first n) (rest n)) - (recur r (inc s) v n)) - 2 (if (map? v) - (recur (assoc r :metadata v) (inc s) (first n) (rest n)) - (recur r (inc s) v n)) - 3 (if (vector? v) - (recur (assoc r :args v) (inc s) (first n) (rest n)) - (throw (ex-info "Invalid macro definition: expected component args vector" {}))) - 4 {:cname (:cname r) - :docs (str (:doc r)) - :arg-props (first (:args r)) - :arg-rest (rest (:args r)) - :body (cons v n) - :meta (:metadata r)}))) - -(defn- prepare-render - [{:keys [cname meta arg-props arg-rest body] :as ctx}] - (let [argsym (gensym "arg") - args (cons argsym arg-rest) - fnbody `(fn ~cname [~@(if arg-props args [])] - (let [~@(cond - (and arg-props (::wrap-props meta true)) - [arg-props `(rumext.util/wrap-props ~argsym)] - - (some? arg-props) - [arg-props argsym] - - :else [])] - ~@(butlast body) - (html ~(last body))))] - - (if (::forward-ref meta) - `(rumext.alpha/forward-ref ~fnbody) - fnbody))) - -(defmacro fnc - [& args] - (let [{:keys [cname meta] :as ctx} (parse-defc args) - wrap-with (or (::wrap meta) - (:wrap meta)) - rfs (gensym "component")] - `(let [~rfs ~(prepare-render ctx)] - (set! (.-displayName ~rfs) ~(str cname)) - ~(if (seq wrap-with) - (reduce (fn [r fi] `(~fi ~r)) rfs wrap-with) - rfs)))) - -(defmacro defc - [& args] - (let [{:keys [cname docs meta] :as ctx} (parse-defc args) - wrap-with (or (::wrap meta) - (:wrap meta)) - rfs (gensym "component")] - `(let [~rfs ~(prepare-render ctx)] - (set! (.-displayName ~rfs) ~(str cname)) - (def ~cname ~docs ~(if (seq wrap-with) - (reduce (fn [r fi] `(~fi ~r)) rfs wrap-with) - rfs)) - ~(when-let [registry (::register meta)] - `(swap! ~registry (fn [state#] (assoc state# ~(::register-as meta (keyword (str cname))) ~cname))))))) - -(defmacro with-memo - [deps & body] - (cond - (vector? deps) - `(rumext.alpha/use-memo - (rumext.alpha/deps ~@deps) - (fn [] ~@body)) - - - (nil? deps) - `(rumext.alpha/use-memo - nil - (fn [] ~@body)) - - :else - `(rumext.alpha/use-memo - (fn [] ~@(cons deps body))))) - -(defmacro with-effect - [deps & body] - (cond - (vector? deps) - `(rumext.alpha/use-effect - (rumext.alpha/deps ~@deps) - (fn [] ~@body)) - - (nil? deps) - `(rumext.alpha/use-effect - nil - (fn [] ~@body)) - - :else - `(rumext.alpha/use-effect - (fn [] ~@(cons deps body))))) diff --git a/src/rumext/alpha.cljs b/src/rumext/alpha.cljs deleted file mode 100644 index 910f78c..0000000 --- a/src/rumext/alpha.cljs +++ /dev/null @@ -1,370 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) Andrey Antukh - -(ns rumext.alpha - (:refer-clojure :exclude [ref deref]) - (:require-macros [rumext.alpha :refer [defc fnc]]) - (:require - ["react" :as react] - ["react-dom" :as rdom] - ["react/jsx-runtime" :as jsxrt] - [cljs.core :as c] - [goog.functions :as gf] - [rumext.util :as util])) - -(def ^:const undefined (js* "(void 0)")) - -(def Component react/Component) -(def Fragment react/Fragment) -(def Profiler react/Profiler) - -(extend-type cljs.core.UUID - INamed - (-name [this] (js* "\"\" + ~{}" this)) - (-namespace [_] "")) - -(defn jsx - ([type props maybe-key] - (jsxrt/jsx type props maybe-key)) - ([type props maybe-key children] - (let [props (js/Object.assign #js {:children children} props)] - (jsxrt/jsx type props maybe-key)))) - -(defn jsxs - ([type props maybe-key] - (jsxrt/jsxs type props maybe-key)) - ([type props maybe-key children] - (let [props (js/Object.assign #js {:children children} props)] - (jsxrt/jsxs type props maybe-key)))) - -(defn forward-ref - [component] - (react/forwardRef component)) - -;; --- Main Api - -(defn mount - "Add element to the DOM tree. Idempotent. Subsequent mounts will - just update element." - [element node] - (rdom/render element node) - nil) - -(defn unmount - "Removes component from the DOM tree." - [node] - (rdom/unmountComponentAtNode node)) - -(defn portal - "Render `element` in a DOM `node` that is ouside of current DOM hierarchy." - [element node] - (rdom/createPortal element node)) - -(defn create-ref - [] - (react/createRef)) - -(defn ref-val - "Given state and ref handle, returns React component." - [ref] - (unchecked-get ref "current")) - -(defn set-ref-val! - [ref val] - (unchecked-set ref "current" val) - val) - -;; --- Context API - -(defn create-context - ([] - (react/createContext nil)) - ([value] - (react/createContext value))) - -(defn provider - [ctx] - (unchecked-get ctx "Provider")) - -;; --- Raw Hooks - -(defn useRef - [initial] - (react/useRef initial)) - -(defn useState - [initial] - (react/useState initial)) - -(defn useEffect - [f deps] - (react/useEffect f deps)) - -(defn useMemo - [f deps] - (react/useMemo f deps)) - -(defn useCallback - [f deps] - (react/useCallback f deps)) - -(defn useLayoutEffect - [f deps] - (react/useLayoutEffect f deps)) - -(defn useContext - [ctx] - (react/useContext ctx)) - -;; --- Hooks - -(defprotocol IDepsAdapter - (adapt [o] "adapt dep if proceed")) - -(extend-protocol IDepsAdapter - default - (adapt [o] o) - - cljs.core.UUID - (adapt [o] (.toString ^js o)) - - cljs.core.Keyword - (adapt [o] (.toString ^js o))) - -;; "A convenience function that translates the list of arguments into a -;; valid js array for use in the deps list of hooks. - -(defn deps - ([] #js []) - ([a] #js [(adapt a)]) - ([a b] #js [(adapt a) (adapt b)]) - ([a b c] #js [(adapt a) (adapt b) (adapt c)]) - ([a b c d] #js [(adapt a) (adapt b) (adapt c) (adapt d)]) - ([a b c d e] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e)]) - ([a b c d e f] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e) (adapt f)]) - ([a b c d e f g] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e) (adapt f) (adapt g)]) - ([a b c d e f g h] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e) (adapt f) (adapt g) (adapt h)]) - ([a b c d e f g h & rest] (into-array (map adapt (into [a b c d e f g h] rest))))) - -;; The cljs version of use-ref and use-ctx is identical to the raw (no -;; customizations/adaptations needed) - -(def use-ref useRef) -(def use-ctx useContext) - -(defn use-effect - ([f] (use-effect #js [] f)) - ([deps f] - (useEffect #(let [r (^function f)] (if (fn? r) r identity)) deps))) - -(defn use-layout-effect - ([f] (use-layout-effect #js [] f)) - ([deps f] - (useLayoutEffect #(let [r (^function f)] (if (fn? r) r identity)) deps))) - -(defn use-memo - ([f] (useMemo f #js [])) - ([deps f] (useMemo f deps))) - -(defn use-callback - ([f] (useCallback f #js [])) - ([deps f] (useCallback f deps))) - -(defn use-fn - "A convenient alias to useCallback" - ([f] (useCallback f #js [])) - ([deps f] (useCallback f deps))) - -(defn deref - [iref] - (let [tmp (useState #(c/deref iref)) - state (aget tmp 0) - set-state (aget tmp 1) - key (useMemo - #(let [key (js/Symbol "rumext.alpha/deref")] - (add-watch iref key (fn [_ _ _ newv] - (^function set-state newv))) - key) - #js [iref])] - - (useEffect - #(do - (^function set-state (c/deref iref)) - (fn [] - (remove-watch iref key))) - #js [iref key]) - - state)) - -(defn use-state - ([] (use-state nil)) - ([initial] - (let [tmp (useState initial) - state (aget tmp 0) - update (aget tmp 1)] - (use-memo - #js [state] - (fn [] - (reify - c/IReset - (-reset! [_ value] - (^function update value)) - - c/ISwap - (-swap! [self f] - (^function update f)) - (-swap! [self f x] - (^function update #(f % x))) - (-swap! [self f x y] - (^function update #(f % x y))) - (-swap! [self f x y more] - (^function update #(apply f % x y more))) - - c/IDeref - (-deref [_] state))))))) - -(defn use-var - "A custom hook for define mutable variables that persists - on renders (based on useRef hook)." - ([] (use-var nil)) - ([initial] - (let [ref (useRef initial)] - (use-memo - #js [] - #(specify! (fn [val] (set-ref-val! ref val)) - c/IReset - (-reset! [_ new-value] - (set-ref-val! ref new-value)) - - c/ISwap - (-swap! - ([self f] - (set-ref-val! ref (f (ref-val ref)))) - ([self f x] - (set-ref-val! ref (f (ref-val ref) x))) - ([self f x y] - (set-ref-val! ref (f (ref-val ref) x y))) - ([self f x y more] - (set-ref-val! ref (apply f (ref-val ref) x y more)))) - - c/IDeref - (-deref [self] (ref-val ref))))))) - -;; --- Other API - -(defn element - ([klass] - (jsx klass #js {} undefined)) - ([klass props] - (let [props (cond - (object? props) props - (map? props) (util/map->obj props) - :else (throw (ex-info "Unexpected props" {:props props})))] - (jsx klass props undefined)))) - -;; --- Higher-Order Components - -(defn memo' - "A raw variant of React.memo." - [component equals?] - (react/memo component equals?)) - -(defn memo - ([component] (react/memo component)) - ([component eq?] - (react/memo component #(util/props-equals? eq? %1 %2)))) - -(defn catch - [component {:keys [fallback on-error]}] - (let [constructor - (fn [props] - (this-as this - (unchecked-set this "state" #js {}) - (.call Component this props))) - - did-catch - (fn [error info] - (when (fn? on-error) - (on-error error info))) - - derive-state - (fn [error] - #js {:error error}) - - render - (fn [] - (this-as this - (let [state (unchecked-get this "state") - props (unchecked-get this "props") - error (unchecked-get state "error")] - (if error - (jsx fallback #js {:error error} undefined) - (jsx component props undefined))))) - - _ (goog/inherits constructor Component) - prototype (unchecked-get constructor "prototype")] - - (unchecked-set constructor "displayName" "ErrorBoundary") - (unchecked-set constructor "getDerivedStateFromError" derive-state) - (unchecked-set prototype "componentDidCatch" did-catch) - (unchecked-set prototype "render" render) - constructor)) - -(def ^:private schedule - (or (and (exists? js/window) js/window.requestAnimationFrame) - #(js/setTimeout % 16))) - -(defn deferred - ([component] (deferred component schedule)) - ([component sfn] - (fnc deferred - {::wrap-props false} - [props] - (let [tmp (useState false) - render? (aget tmp 0) - set-render (aget tmp 1)] - (use-effect (fn [] (^function sfn #(^function set-render true)))) - (when ^boolean render? - [:> component props]))))) - -(defn throttle - [component ms] - (fnc throttle - {::wrap-props false} - [props] - (let [tmp (useState props) - state (aget tmp 0) - set-state (aget tmp 1) - - ref (useRef false) - render (useMemo - #(gf/throttle - (fn [v] - (when-not ^boolean (ref-val ref) - (^function set-state v))) - ms) - #js [])] - (useEffect #(^function render props) #js [props]) - (useEffect #(fn [] (set-ref-val! ref true)) #js []) - [:> component state]))) - -(defn check-props - "Utility function to use with `memo'`. - Will check the `props` keys to see if they are equal. - - Usage: - - (mf/defc my-component - {::mf/wrap [#(mf/memo' % (checkprops [\"prop1\" \"prop2\"]))]} - [props] - )" - - ([props] (check-props props =)) - ([props eqfn?] - (fn [np op] - (every? #(eqfn? (unchecked-get np %) - (unchecked-get op %)) - props)))) diff --git a/src/rumext/compiler.cljc b/src/rumext/compiler.cljc deleted file mode 100644 index f7bf3a8..0000000 --- a/src/rumext/compiler.cljc +++ /dev/null @@ -1,265 +0,0 @@ -(ns rumext.compiler - " - Hicada - Hiccup compiler aus dem Allgaeu - - NOTE: The code for has been forked like this: - weavejester/hiccup -> r0man/sablono -> Hicada -> rumext" - (:refer-clojure :exclude [compile]) - (:require - [rumext.normalize :as norm] - [rumext.util :as util])) - -(def ^:dynamic *handlers* nil) - -(def default-handlers - {:> (fn [_ klass attrs & children] - [klass attrs children]) - :& (fn - ([_ klass] - (let [klass klass] - [klass {} nil])) - ([_ klass props & children] - (let [klass klass] - (if (map? props) - [klass (rumext.util/compile-map->object props) children] - [klass (list 'rumext.util/map->obj props) children])))) - :* (fn [_ attrs & children] - (if (map? attrs) - ['rumext.alpha/Fragment attrs children] - ['rumext.alpha/Fragment {} (cons attrs children)]))}) - -(declare emit-react) - -(defn- compile-class-attr - [value] - (cond - (or (nil? value) - (keyword? value) - (string? value)) - value - - (and (or (sequential? value) - (set? value)) - (every? string? value)) - (util/join-classes value) - - (vector? value) - (apply util/compile-join-classes value) - - :else value)) - -(defn compile-attr - [[key val :as kvpair]] - (cond - (= key :class) [:className (compile-class-attr val)] - (= key :style) [key (util/camel-case-keys val)] - (= key :for) [:htmlFor val] - (or (keyword? key) - (symbol? key)) [(util/camel-case key) val] - :else kvpair)) - -(declare compile*) - -(defmulti compile-form - "Pre-compile certain standard forms, where possible." - (fn [form] - (when (and (seq? form) (symbol? (first form))) - (name (first form))))) - -(defmethod compile-form "do" - [[_ & forms]] - `(do ~@(butlast forms) ~(compile* (last forms)))) - -(defmethod compile-form "array" - [[_ & forms]] - `(cljs.core/array ~@(mapv compile* forms))) - -(defmethod compile-form "let" - [[_ bindings & body]] - `(let ~bindings ~@(butlast body) ~(compile* (last body)))) - -(defmethod compile-form "let*" - [[_ bindings & body]] - `(let* ~bindings ~@(butlast body) ~(compile* (last body)))) - -(defmethod compile-form "letfn*" - [[_ bindings & body]] - `(letfn* ~bindings ~@(butlast body) ~(compile* (last body)))) - -(defmethod compile-form "for" - [[_ bindings body]] - ;; Special optimization: For a simple (for [x xs] ...) we rewrite the for - ;; to a fast reduce outputting a JS array: - (if (== 2 (count bindings)) - (let [[item coll] bindings] - `(reduce (fn [out-arr# ~item] - (.push out-arr# ~(compile* body)) - out-arr#) - (cljs.core/array) ~coll)) - ;; Still optimize a little by giving React an array: - (list 'cljs.core/into-array `(for ~bindings ~(compile* body))))) - -(defmethod compile-form "if" - [[_ condition & body]] - `(if ~condition ~@(doall (for [x body] (compile* x))))) - -(defmethod compile-form "when" - [[_ bindings & body]] - `(when ~bindings ~@(doall (for [x body] (compile* x))))) - -(defmethod compile-form "when-some" - [[_ bindings & body]] - `(when-some ~bindings ~@(butlast body) ~(compile* (last body)))) - -(defmethod compile-form "when-let" - [[_ bindings & body]] - `(when-let ~bindings ~@(butlast body) ~(compile* (last body)))) - -(defmethod compile-form "when-first" - [[_ bindings & body]] - `(when-first ~bindings ~@(butlast body) ~(compile* (last body)))) - -(defmethod compile-form "when-not" - [[_ bindings & body]] - `(when-not ~bindings ~@(doall (for [x body] (compile* x))))) - -(defmethod compile-form "if-not" - [[_ bindings & body]] - `(if-not ~bindings ~@(doall (for [x body] (compile* x))))) - -(defmethod compile-form "if-some" - [[_ bindings & body]] - `(if-some ~bindings ~@(doall (for [x body] (compile* x))))) - -(defmethod compile-form "if-let" - [[_ bindings & body]] - `(if-let ~bindings ~@(doall (for [x body] (compile* x))))) - -(defmethod compile-form "letfn" - [[_ bindings & body]] - `(letfn ~bindings ~@(butlast body) ~(compile* (last body)))) - -(defmethod compile-form "fn" - [[_ params & body]] - `(fn ~params ~@(butlast body) ~(compile* (last body)))) - -(defmethod compile-form "case" - [[_ v & cases]] - `(case ~v - ~@(doall (mapcat - (fn [[test hiccup]] - (if hiccup - [test (compile* hiccup)] - [(compile* test)])) - (partition-all 2 cases))))) - -(defmethod compile-form "condp" - [[_ f v & cases]] - `(condp ~f ~v - ~@(doall (mapcat - (fn [[test hiccup]] - (if hiccup - [test (compile* hiccup)] - [(compile* test)])) - (partition-all 2 cases))))) - -(defmethod compile-form "cond" - [[_ & clauses]] - `(cond ~@(doall - (mapcat - (fn [[check expr]] [check (compile* expr)]) - (partition 2 clauses))))) - -(defmethod compile-form :default [expr] expr) - -(defn compile-element - "Returns an unevaluated form that will render the supplied vector as a HTML element." - [[tag attrs & children :as element]] - (cond - ;; e.g. [:> Component {:key "xyz", :foo "bar} ch0 ch1] - (contains? *handlers* tag) - (let [f (get *handlers* tag) - [klass attrs children] (apply f element)] - (emit-react klass attrs (mapv compile* children))) - - ;; e.g. [:span {} x] - (and (util/literal? tag) (map? attrs)) - (let [[tag attrs _] (norm/element [tag attrs])] - (emit-react tag attrs (mapv compile* children))) - - (util/literal? tag) - ;; We could now interpet this as either: - ;; 1. First argument is the attributes (in #js{} provided by the user) OR: - ;; 2. First argument is the first child element. - ;; We assume #2. Always! - (compile-element (list* tag {} attrs children)) - - ;; Problem: [a b c] could be interpreted as: - ;; 1. The coll of ReactNodes [a b c] OR - ;; 2. a is a React Element, b are the props and c is the first child - ;; We default to 1) (handled below) BUT, if b is a map, we know this must be 2) - ;; since a map doesn't make any sense as a ReactNode. - ;; [foo {...} ch0 ch1] NEVER makes sense to interpret as a sequence - (and (vector? element) (map? attrs)) - (emit-react tag attrs (mapv compile* children)) - - (seq? element) - (seq (mapv compile* element)) - - ;; We have nested children - ;; [[:div "foo"] [:span "foo"]] - :else - (mapv compile* element))) - -(defn compile* - "Pre-compile data structures" - [content] - (cond - (vector? content) (compile-element content) - (util/literal? content) content - :else (compile-form content))) - -(defn tag->el - [x] - (assert (or (symbol? x) (keyword? x) (string? x) (seq? x)) - (str "Got: " (#?(:clj class :cljs type) x))) - (if (keyword? x) - (name x) - x)) - -(def props-xform - (comp - (remove (fn [[k v]] (= k :key))) - (map compile-attr))) - -(defn emit-react - "Emits the final react js code" - [tag attrs children] - (let [tag (tag->el tag) - children (into [] (filter some?) children) - [key props] (if (map? attrs) - [(or (:key attrs) - 'rumext.alpha/undefined) - (->> (into {} props-xform attrs) - (util/compile-to-js))] - ['rumext.alpha/undefined attrs])] - (cond - (= 0 (count children)) - (list 'rumext.alpha/jsx tag props key) - - (= 1 (count children)) - (list 'rumext.alpha/jsx tag props key (first children)) - - :else - (list 'rumext.alpha/jsxs tag props key (apply list 'cljs.core/array children))))) - -(defn compile - "Arguments: - - content: The hiccup to compile - - handlers: A map to handle special tags. See default-handlers in this namespace. - " - ([content] - (compile content nil)) - ([content handlers] - (binding [*handlers* (merge default-handlers handlers)] - (compile* content)))) diff --git a/src/rumext/util.cljc b/src/rumext/util.cljc deleted file mode 100644 index adba4f6..0000000 --- a/src/rumext/util.cljc +++ /dev/null @@ -1,186 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2016-2020 Andrey Antukh - -(ns rumext.util - (:require - [clojure.string :as str] - [clojure.set :as set])) - -(defn compile-to-js - [form] - "Compile a statically known data sturcture, recursivelly to js - expression. Mainly used by macros for create js data structures at - compile time." - (cond - (map? form) - (if (empty? form) - (list 'js* "{}") - (let [key-strs (mapv compile-to-js (keys form)) - non-str (remove string? key-strs) - _ (assert (empty? non-str) - (str "Rumext: Props can't be dynamic:" - (pr-str non-str) "in: " (pr-str form))) - kvs-str (->> (mapv #(-> (str \' % "':~{}")) key-strs) - (interpose ",") - (apply str))] - (vary-meta - (list* 'js* (str "{" kvs-str "}") (mapv compile-to-js (vals form))) - assoc :tag 'object))) - - (vector? form) - (apply list 'cljs.core/array (mapv compile-to-js form)) - - (keyword? form) - (name form) - - :else form)) - -(defn compile-map->object - "Compile a statically known clojure map to js object - expression. Mainly used by macros for create js objects at - compile time (component props)." - [m] - (if (empty? m) - (list 'js* "{}") - (let [key-strs (mapv compile-to-js (keys m)) - non-str (remove string? key-strs) - _ (assert (empty? non-str) - (str "Rumext: Props can't be dynamic:" - (pr-str non-str) "in: " (pr-str m))) - kvs-str (->> (mapv #(-> (str \' % "':~{}")) key-strs) - (interpose ",") - (apply str))] - (vary-meta - (list* 'js* (str "{" kvs-str "}") (mapv identity (vals m))) - assoc :tag 'object)))) - -#?(:cljs - (defn obj->map - "Convert shallowly an js object to cljs map." - [obj] - (let [keys (.keys js/Object obj) - len (alength keys)] - (loop [i 0 - r (transient {})] - (if (< i len) - (let [key (aget keys i)] - (recur (unchecked-inc i) - (assoc! r (keyword key) (unchecked-get obj key)))) - (persistent! r)))))) - -#?(:cljs - (defn map->obj - [o] - (let [m #js {}] - (run! (fn [[k v]] (unchecked-set m (name k) v)) o) - m))) - -#?(:cljs - (defn wrap-props - [props] - (cond - (object? props) (obj->map props) - (map? props) props - (nil? props) {} - :else (throw (ex-info "Unexpected props" {:props props}))))) - -#?(:cljs - (defn props-equals? - [eq? new-props old-props] - (let [old-keys (.keys js/Object old-props) - new-keys (.keys js/Object new-props) - old-keys-len (alength old-keys) - new-keys-len (alength new-keys)] - (if (identical? old-keys-len new-keys-len) - (loop [idx (int 0)] - (if (< idx new-keys-len) - (let [key (aget new-keys idx) - new-val (unchecked-get new-props key) - old-val (unchecked-get old-props key)] - (if ^boolean (eq? new-val old-val) - (recur (inc idx)) - false)) - true)) - false)))) - -#?(:cljs - (defn symbol-for - [v] - (.for js/Symbol v))) - -(defn camel-case - "Returns camel case version of the key, e.g. :http-equiv becomes :httpEquiv." - [k] - (if (or (keyword? k) - (string? k) - (symbol? k)) - (let [[first-word & words] (str/split (name k) #"-")] - (if (or (empty? words) - (= "aria" first-word) - (= "data" first-word)) - k - (-> (map str/capitalize words) - (conj first-word) - str/join - keyword))) - k)) - -(defn camel-case-keys - "Recursively transforms all map keys into camel case." - [m] - (cond - (map? m) - (reduce-kv - (fn [m k v] - (assoc m (camel-case k) v)) - {} m) - ;; React native accepts :style [{:foo-bar ..} other-styles] so camcase those keys: - (vector? m) - (mapv camel-case-keys m) - :else - m)) - -(defn element? - "- is x a vector? - AND - - first element is a keyword?" - [x] - (and (vector? x) (keyword? (first x)))) - -(defn unevaluated? - "True if the expression has not been evaluated. - - expr is a symbol? OR - - it's something like (foo bar)" - [expr] - (or (symbol? expr) - (and (seq? expr) - (not= (first expr) `quote)))) - -(defn literal? - "True if x is a literal value that can be rendered as-is." - [x] - (and (not (unevaluated? x)) - (or (not (or (vector? x) (map? x))) - (and (every? literal? x) - (not (keyword? (first x))))))) - -(defn join-classes - "Join the `classes` with a whitespace." - [classes] - (->> (map #(if (string? %) % (seq %)) classes) - (flatten) - (remove nil?) - (str/join " "))) - -(defn compile-join-classes - "Joins strings space separated" - ([] "") - ([& xs] - (let [strs (->> (repeat (count xs) "~{}") - (interpose ",") - (apply str))] - (list* 'js* (str "[" strs "].join(' ')") xs)))) - diff --git a/src/rumext/v2.clj b/src/rumext/v2.clj new file mode 100644 index 0000000..e3e9d34 --- /dev/null +++ b/src/rumext/v2.clj @@ -0,0 +1,499 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) Andrey Antukh + +(ns rumext.v2 + (:refer-clojure :exclude [simple-ident?]) + (:require + [cljs.core :as-alias c] + [clojure.string :as str] + [rumext.v2.compiler :as hc] + [rumext.v2.util :as util])) + +(create-ns 'rumext.v2.util) + +(defn ^:no-doc production-build? + [] + (let [env (System/getenv)] + (or (= "production" (get env "NODE_ENV")) + (= "production" (get env "RUMEXT_ENV")) + (= "production" (get env "TARGET_ENV"))))) + +(defmacro html + [body] + (hc/compile body)) + +(defn parse-defc + [args] + (loop [r {} + s 0 + v (first args) + n (rest args)] + (case s + 0 (if (symbol? v) + (recur (assoc r :cname v) (inc s) (first n) (rest n)) + (recur (assoc r :cname (gensym "anonymous-")) (inc s) v n)) + 1 (if (string? v) + (recur (assoc r :doc v) (inc s) (first n) (rest n)) + (recur r (inc s) v n)) + 2 (if (map? v) + (recur (assoc r :meta v) (inc s) (first n) (rest n)) + (recur r (inc s) v n)) + 3 (if (vector? v) + (recur (assoc r :args v) (inc s) (first n) (rest n)) + (throw (ex-info "Invalid macro definition: expected component args vector" {}))) + + (let [psym (with-meta (gensym "props-") {:tag 'js})] + {:cname (:cname r) + :docs (str (:doc r)) + :props (first (:args r)) + :params (into [psym] (rest (:args r))) + :body (cons v n) + :psym psym + :meta (:meta r)})))) + +(defn- wrap-props? + [{:keys [cname meta]}] + (let [default-style (if (str/ends-with? (name cname) "*") :obj :clj)] + (cond + (contains? meta ::props) + (= :clj (get meta ::props default-style)) + + (contains? meta ::wrap-props) + (get meta ::wrap-props) + + (str/ends-with? (name cname) "*") + false + + :else + true))) + +(defn- react-props? + [{:keys [meta cname] :as ctx}] + (and (not (wrap-props? ctx)) + (or (str/ends-with? (name cname) "*") + (= (::props meta) :react)))) + +(defn- simple-ident? + [s] + (some? (re-matches #"[A-Za-z0-9_]+" s))) + +(defn- prepare-let-bindings + [{:keys [cname meta props body params] :as ctx}] + (let [react-props? (react-props? ctx) + psym (first params)] + (cond + (and (some? props) (wrap-props? ctx)) + [props (list 'rumext.v2.util/wrap-props psym)] + + (and (map? props) (not (wrap-props? ctx))) + (let [alias (get props :as) + alts (get props :or) + other (or (get props :&) + (get props :rest)) + items (some-> (get props :keys) set)] + (cond->> [] + (symbol? alias) + (into [alias psym]) + + (symbol? other) + (into [other (list 'js* "undefined")]) + + (set? items) + (concat (mapcat (fn [k] + (let [prop-name (if react-props? + (util/ident->prop k) + (name k)) + accessor (if (simple-ident? prop-name) + (list '. psym (symbol (str "-" prop-name))) + (list 'cljs.core/unchecked-get psym prop-name))] + + [(if (symbol? k) k (symbol prop-name)) + (cond + ;; If the other symbol is present, then a + ;; different destructuring stragegy will be + ;; used so we need to set here the value to + ;; 'undefined' + (symbol? other) + (list 'js* "undefined") + + (contains? alts k) + `(~'js* "~{} ?? ~{}" ~accessor ~(get alts k)) + + :else + accessor)])) + items)))) + + (symbol? props) + [props psym]))) + +(defn native-destructure + "Generates a js var line with native destructuring. Only used when :& + used in destructuring." + [{:keys [props params props] :as ctx}] + + ;; Emit native destructuring only if the :& key has value + (when (or (symbol? (:& props)) + (symbol? (:rest props))) + + (let [react-props? (react-props? ctx) + psym (first params) + + keys-props (:keys props []) + all-alias (:as props) + rst-alias (or (:& props) (:rest props)) + + k-props (dissoc props :keys :as :& :rest) + k-props (->> (:keys props []) + (map (fn [k] + (let [kv (if react-props? + (util/ident->prop k) + (name k))] + [k kv]))) + (into k-props)) + + props [] + params [] + + [props params] + (if (seq k-props) + (reduce (fn [[props params] [ks kp]] + (let [kp (if react-props? + (util/ident->prop kp) + (name kp))] + [(conj props (str "~{}: ~{}")) + (conj params kp ks)])) + [props params] + k-props) + [props params]) + + [props params] + (if (symbol? rst-alias) + [(conj props "...~{}") (conj params rst-alias)] + [props params]) + + tmpl (str "var {" + (str/join ", " props) + "} = ~{}") + params (conj params psym)] + + [(apply list 'js* tmpl params)]))) + +(defn- prepare-props-checks + [{:keys [cname meta params] :as ctx}] + (let [react-props? (react-props? ctx) + psym (vary-meta (first params) assoc :tag 'js)] + (when *assert* + (cond + (::schema meta) + (let [validator-sym (with-meta (symbol (str cname "-validator")) + {:tag 'function})] + (concat + (cons (list 'js* "// ===== start props checking =====") nil) + [`(let [res# (~validator-sym ~psym)] + (when (some? res#) + (let [items# (reduce-kv (fn [result# k# v#] + (conj result# (str " -> '" k# "' " v# ""))) + [] + res#) + msg# (str ~(str "invalid props on component " (str cname) "\n\n") + (str/join "\n" items#) + "\n")] + (throw (js/Error. msg#)))))] + (cons (list 'js* "// ===== end props checking =====") nil))) + + (::expect meta) + (let [props (::expect meta)] + (concat + (cons (list 'js* "// ===== start props checking =====") nil) + (if (map? props) + (->> props + (map (fn [[prop pred-sym]] + (let [prop (if react-props? + (util/ident->prop prop) + (name prop)) + + accs (if (simple-ident? prop) + (list '. psym (symbol (str "-" prop))) + (list 'cljs.core/unchecked-get psym prop)) + + expr `(~pred-sym ~accs)] + `(when-not ~(vary-meta expr assoc :tag 'boolean) + (throw (js/Error. ~(str "invalid value for '" prop "'")))))))) + + (->> props + (map (fn [prop] + (let [prop (if react-props? + (util/ident->prop prop) + (name prop)) + expr `(.hasOwnProperty ~psym ~prop)] + `(when-not ~(vary-meta expr assoc :tag 'boolean) + (throw (js/Error. ~(str "missing prop '" prop "'"))))))))) + (cons (list 'js* "// ===== end props checking =====") nil))) + + :else + [])))) + +(defn- prepare-render-fn + [{:keys [cname meta body params props] :as ctx}] + (let [f `(fn ~cname ~params + ~@(prepare-props-checks ctx) + (let [~@(prepare-let-bindings ctx)] + ~@(native-destructure ctx) + + ~@(butlast body) + ~(hc/compile (last body))))] + (if (::forward-ref meta) + `(rumext.v2/forward-ref ~f) + f))) + +(defn- resolve-wrappers + [{:keys [cname docs meta] :as ctx}] + (let [wrappers (or (::wrap meta) (:wrap meta) []) + react-props? (react-props? ctx) + memo (::memo meta)] + (cond + (set? memo) + (let [eq-f (or (::memo-equals ctx) 'cljs.core/=) + np-s (with-meta (gensym "new-props-") {:tag 'js}) + op-s (with-meta (gensym "old-props-") {:tag 'js}) + op-f (fn [prop] + (let [prop (if react-props? + (util/ident->prop prop) + (name prop)) + accs (if (simple-ident? prop) + (let [prop (symbol (str "-" (name prop)))] + (list eq-f + (list '.. np-s prop) + (list '.. op-s prop))) + (list eq-f + (list 'cljs.core/unchecked-get np-s prop) + (list 'cljs.core/unchecked-get op-s prop)))] + (with-meta accs {:tag 'boolean})))] + (conj wrappers + `(fn [component#] + (mf/memo' component# (fn [~np-s ~op-s] + (and ~@(map op-f memo))))))) + + (true? memo) + (if-let [eq-f (::memo-equals meta)] + (conj wrappers `(fn [component#] + (mf/memo component# ~eq-f))) + (conj wrappers 'rumext.v2/memo')) + + :else wrappers))) + +(defmacro fnc + "A macro for defining inline component functions. Look the user guide for + understand how to use it." + [& args] + (let [{:keys [cname meta] :as ctx} (parse-defc args) + wrappers (resolve-wrappers ctx) + rfs (gensym (str cname "__"))] + `(let [~rfs ~(if (seq wrappers) + (reduce (fn [r fi] `(~fi ~r)) (prepare-render-fn ctx) wrappers) + (prepare-render-fn ctx))] + ~@(when-not (production-build?) + [`(set! (.-displayName ~rfs) ~(str cname))]) + ~rfs))) + +(defmacro defc + "A macro for defining component functions. Look the user guide for + understand how to use it." + [& args] + (let [{:keys [cname docs meta] :as ctx} (parse-defc args) + wrappers (resolve-wrappers ctx) + react-props? (react-props? ctx) + cname (if (::private meta) + (vary-meta cname assoc :private true) + cname)] + `(do + ~@(when (and (::schema meta) react-props? *assert*) + (let [validator-sym (with-meta (symbol (str cname "-validator")) + {:tag 'function})] + [`(def ~validator-sym (rumext.v2.validation/validator ~(::schema meta)))])) + + (def ~cname ~docs ~(if (seq wrappers) + (reduce (fn [r fi] `(~fi ~r)) (prepare-render-fn ctx) wrappers) + (prepare-render-fn ctx))) + + ~@(when-not (production-build?) + [`(set! (.-displayName ~cname) ~(str cname))]) + + ~(when-let [registry (::register meta)] + `(swap! ~registry (fn [state#] (assoc state# ~(::register-as meta (keyword (str cname))) ~cname))))))) + +(defmacro deps + "A convenience macro version of mf/deps function" + [& params] + `(cljs.core/array ~@(map (fn [s] `(rumext.v2/adapt ~s)) params))) + +(defmacro with-memo + "A convenience syntactic abstraction (macro) for `useMemo`" + [deps & body] + (cond + (vector? deps) + `(rumext.v2/use-memo + (rumext.v2/deps ~@deps) + (fn [] ~@body)) + + + (nil? deps) + `(rumext.v2/use-memo + nil + (fn [] ~@body)) + + :else + `(rumext.v2/use-memo + (fn [] ~@(cons deps body))))) + +(defmacro ^:no-doc with-fn + [deps & body] + (cond + (vector? deps) + `(rumext.v2/use-fn + (rumext.v2/deps ~@deps) + ~@body) + + + (nil? deps) + `(rumext.v2/use-fn + nil + ~@body) + + :else + `(rumext.v2/use-fn + ~@(cons deps body)))) + +(defmacro with-effect + "A convenience syntactic abstraction (macro) for `useEffect`" + [deps & body] + (cond + (vector? deps) + `(rumext.v2/use-effect + (rumext.v2/deps ~@deps) + (fn [] ~@body)) + + (nil? deps) + `(rumext.v2/use-effect + nil + (fn [] ~@body)) + + :else + `(rumext.v2/use-effect + (fn [] ~@(cons deps body))))) + +(defmacro with-layout-effect + "A convenience syntactic abstraction (macro) for `useLayoutEffect`" + [deps & body] + (cond + (vector? deps) + `(rumext.v2/use-layout-effect + (rumext.v2/deps ~@deps) + (fn [] ~@body)) + + (nil? deps) + `(rumext.v2/use-layout-effect + nil + (fn [] ~@body)) + + :else + `(rumext.v2/use-layout-effect + (fn [] ~@(cons deps body))))) + +(defmacro check-props + "A macro version of the `check-props` function" + [props & [eq-f :as rest]] + (if (symbol? props) + `(apply rumext.v2/check-props ~props ~rest) + + (let [eq-f (or eq-f 'cljs.core/=) + np-s (with-meta (gensym "new-props-") {:tag 'js}) + op-s (with-meta (gensym "old-props-") {:tag 'js}) + op-f (fn [prop] + (let [prop-access (symbol (str "-" (name prop)))] + (with-meta + (if (simple-ident? prop) + (list eq-f + (list '.. np-s prop-access) + (list '.. op-s prop-access)) + (list eq-f + (list 'cljs.core/unchecked-get np-s prop) + (list 'cljs.core/unchecked-get op-s prop))) + {:tag 'boolean})))] + `(fn [~np-s ~op-s] + (and ~@(map op-f props)))))) + +(defmacro lazy-component + "A macro that helps defining lazy-loading components with the help + of shadow-cljs tooling." + [ns-sym] + (if (production-build?) + `(let [loadable# (shadow.lazy/loadable ~ns-sym)] + (rumext.v2/lazy (fn [] + (.then (shadow.lazy/load loadable#) + (fn [component#] + (cljs.core/js-obj "default" component#)))))) + `(let [loadable# (shadow.lazy/loadable ~ns-sym)] + (rumext.v2/lazy (fn [] + (.then (shadow.lazy/load loadable#) + (fn [_#] + (cljs.core/js-obj "default" + (rumext.v2/fnc ~'wrapper + {:rumext.v2/props :obj} + [props#] + [:> (deref loadable#) props#]))))))))) + +(defmacro spread-object + "A helper for spread two js objects, adapting compile time known + keys to cameCase. + + You can pass `:rumext.v2/transform false` on `other` metadata + for disable key casing transformation." + [target other] + (assert (or (symbol? target) + (map? target)) + "only symbols or maps accepted on target") + + (assert (or (symbol? other) + (map? other)) + "only symbols or map allowed for the spread") + + (let [transform? (get (meta other) ::transform true) + compile-prop (if transform? + (partial hc/compile-prop 2) + identity)] + (hc/compile-to-js-spread target other compile-prop))) + +(defmacro spread-props + "A helper for spread two js objects using react conventions for + compile time known props keys names." + [target other] + (assert (or (symbol? target) + (map? target)) + "only symbols or maps accepted on target") + + (assert (or (symbol? other) + (map? other)) + "only symbols or map allowed for the spread") + + (hc/compile-to-js-spread target other hc/compile-prop)) + +(defmacro spread + "A shorter alias for spread props" + [target other] + `(spread-props ~target ~other)) + +(defmacro props + "A helper for convert literal datastructures into js data + structures at compile time using react props convention." + [value] + (let [recursive? (get (meta value) ::recursive false)] + (hc/compile-props-to-js value ::hc/transform-props-recursive recursive?))) + +(defmacro object + [value] + (let [recursive? (get (meta value) ::recursive true)] + (hc/compile-coll-to-js value ::hc/transform-props-recursive recursive?))) diff --git a/src/rumext/v2.cljs b/src/rumext/v2.cljs new file mode 100644 index 0000000..64fb489 --- /dev/null +++ b/src/rumext/v2.cljs @@ -0,0 +1,511 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) Andrey Antukh + +(ns rumext.v2 + (:refer-clojure :exclude [ref deref use]) + (:require-macros [rumext.v2 :refer [defc fnc]]) + (:require + ["react" :as react] + ["react-dom" :as rdom] + ["react-dom/client" :as rdomc] + ["react/jsx-runtime" :as jsxrt] + [cljs.core :as c] + [goog.functions :as gf] + [rumext.v2.util :as util] + [rumext.v2.validation] + [shadow.lazy])) + +(def ^:const undefined (js* "(void 0)")) + +(def browser-context? + "A boolean var, indicates if the current code is running on browser main thread or not." + (exists? js/window)) + +(def Component + "The `react.Component` class" + react/Component) + +(def Fragment + "The `react.Fragment class" + react/Fragment) + +(def Profiler + "The `react.Profiler` class" + react/Profiler) + +(def Suspense + "The `react.Suspense` class" + react/Suspense) + +(extend-type cljs.core.UUID + INamed + (-name [this] (js* "\"\" + ~{}" this)) + (-namespace [_] "")) + +(def ^:no-doc ^function jsx jsxrt/jsx) +(def ^:no-doc ^function jsxs jsxrt/jsxs) + +(defn merge-props + [props1 props2] + (js/Object.assign #js {} props1 props2)) + +(def ^function forward-ref + "lets your component expose a DOM node to parent component with a ref." + react/forwardRef) + +;; --- Main Api + +(def ^function portal + "Render `element` in a DOM `node` that is ouside of current DOM hierarchy." + rdom/createPortal) + +(def ^function create-root + "Creates react root" + rdomc/createRoot) + +(def hydrate-root + "Lets you display React components inside a browser DOM node whose + HTML content was previously generated by react-dom/server" + rdomc/hydrateRoot) + +(defn render! + [root element] + (.render ^js root element)) + +(defn unmount! + "Removes component from the DOM tree." + [root] + (.unmount ^js root)) + +(def ^function create-ref react/createRef) + +(defn ref-val + "Given state and ref handle, returns React component." + [ref] + (unchecked-get ref "current")) + +(defn set-ref-val! + [ref val] + (unchecked-set ref "current" val) + val) + +(def ^function lazy + "A helper for creating lazy loading components." + react/lazy) + +;; --- Context API + +(def ^function create-context + "Create a react context" + react/createContext) + +(defn provider + "Get the current provider for specified context" + [ctx] + (unchecked-get ctx "Provider")) + +;; --- Raw Hooks + +(def ^function useId + "The `react.useId` hook function" + react/useId) + +(def ^function useRef + "The `react.useRef` hook function" + react/useRef) + +(def ^function useState + "The `react.useState` hook function" + react/useState) + +(def ^function useEffect + "The `react.useEffect` hook function" + react/useEffect) + +(def ^function useInsertionEffect + "The react.useInsertionEffect` hook function" + react/useInsertionEffect) + +(def ^function useLayoutEffect + "The `react.useLayoutEffect` hook function" + react/useLayoutEffect) + +(def ^function useDeferredValue + "The `react.useDeferredValue hook function" + react/useDeferredValue) + +(def ^function useMemo + "The `react.useMemo` hook function" + react/useMemo) + +(def ^function useCallback + "The `react.useCallback` hook function" + react/useCallback) + +(def ^function useContext + "The `react.useContext` hook function" + react/useContext) + +(def ^function useTransition + "The `react.useTransition` hook function" + react/useTransition) + +;; --- Hooks + +(def ^function use + "The `react.use` helper" + react/use) + +(def ^:private adapt-sym + (js/Symbol "rumext:adapt-fn")) + +(unchecked-set (.-prototype cljs.core/UUID) + adapt-sym + (fn [o] (.-uuid ^cljs.core/UUID o))) + +(unchecked-set (.-prototype cljs.core/Keyword) + adapt-sym + (fn [o] (.toString ^js o))) + +(unchecked-set (.-prototype cljs.core/Symbol) + adapt-sym + (fn [o] (.toString ^js o))) + +(defn adapt + [o] + (when (some? o) + (let [adapt-fn (unchecked-get o adapt-sym)] + (if ^boolean adapt-fn + (^function adapt-fn o) + o)))) + +(defn deps + "A helper for creating hook deps array, that handles some + adaptations for clojure specific data types such that UUID and + keywords" + ([] #js []) + ([a] #js [(adapt a)]) + ([a b] #js [(adapt a) (adapt b)]) + ([a b c] #js [(adapt a) (adapt b) (adapt c)]) + ([a b c d] #js [(adapt a) (adapt b) (adapt c) (adapt d)]) + ([a b c d e] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e)]) + ([a b c d e f] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e) (adapt f)]) + ([a b c d e f g] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e) (adapt f) (adapt g)]) + ([a b c d e f g h] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e) (adapt f) (adapt g) (adapt h)]) + ([a b c d e f g h & rest] (into-array (map adapt (into [a b c d e f g h] rest))))) + +(def ^function use-ref + "A lisp-case alias for `useRef`" + react/useRef) + +(def ^function use-ctx + "A lisp-case short alias for the `useContext` hook function" + react/useContext) + +(def ^function use-id + "A lisp-case alias fro `useId` hook function" + react/useId) + +(def ^function start-transition + "An alias for react.startTransition function" + react/startTransition) + +(def noop (constantly nil)) + +(defn use-effect + "A rumext variant of the `useEffect` hook function with order of + arguments inverted" + ([f] (use-effect #js [] f)) + ([deps f] + (useEffect #(let [r (^function f)] (if (fn? r) r noop)) deps))) + +(defn use-insertion-effect + "A rumext variant of the `useInsertionEffect` hook function with order + of arguments inverted" + ([f] (use-insertion-effect #js [] f)) + ([deps f] + (useInsertionEffect #(let [r (^function f)] (if (fn? r) r noop)) deps))) + +(defn use-layout-effect + "A rumext variant of the `useLayoutEffect` hook function with order + of arguments inverted" + ([f] (use-layout-effect #js [] f)) + ([deps f] + (useLayoutEffect #(let [r (^function f)] (if (fn? r) r noop)) deps))) + +(defn use-ssr-effect + "An EXPERIMENTAL use-effect version that detects if we are in a NON + browser context and runs the effect fn inmediatelly." + [deps effect-fn] + (if ^boolean browser-context? + (use-effect deps effect-fn) + (let [ret (effect-fn)] + (when (fn? ret) + (ret))))) + +(defn use-memo + "A rumext variant of the `useMemo` hook function with order + of arguments inverted" + ([f] (useMemo f #js [])) + ([deps f] (useMemo f deps))) + +(defn use-transition + "A rumext version of the `useTransition` hook function. Returns a + function object that implements the IPending protocol for check the + state of the transition." + [] + (let [tmp (useTransition) + is-pending (aget tmp 0) + start-fn (aget tmp 1)] + (use-memo + #js [is-pending] + (fn [] + (specify! (fn [cb-fn] + (^function start-fn cb-fn)) + cljs.core/IPending + (-realized? [_] (not ^boolean is-pending))))))) + +(defn use-callback + "A rumext variant of the `useCallback` hook function with order + of arguments inverted" + ([f] (useCallback f #js [])) + ([deps f] (useCallback f deps))) + +(defn use-fn + "A convenience short alias for `use-callback`" + ([f] (useCallback f #js [])) + ([deps f] (useCallback f deps))) + +(defn deref + "A rumext hook for deref and watch an atom or atom like object. It + internally uses the react.useSyncExternalSource API" + [iref] + (let [state (use-ref (c/deref iref)) + key (use-id) + get-state (use-fn #js [state] #(unchecked-get state "current")) + subscribe (use-fn #js [iref key] + (fn [listener-fn] + (unchecked-set state "current" (c/deref iref)) + (add-watch iref key (fn [_ _ _ newv] + (unchecked-set state "current" newv) + (^function listener-fn))) + #(remove-watch iref key))) + snapshot (use-fn #js [iref] #(c/deref iref))] + (react/useSyncExternalStore subscribe get-state snapshot))) + +(deftype State [update-fn value] + c/IReset + (-reset! [_ value] + (^function update-fn value)) + + c/ISwap + (-swap! [self f] + (^function update-fn f)) + (-swap! [self f x] + (^function update-fn #(f % x))) + (-swap! [self f x y] + (^function update-fn #(f % x y))) + (-swap! [self f x y more] + (^function update-fn #(apply f % x y more))) + + c/IDeref + (-deref [_] value)) + +(defn use-state + "A rumext variant of `useState`. Returns an object that implements + the Atom protocols." + ([] (use-state nil)) + ([initial] + (let [tmp (useState initial) + ref (useRef nil) + value (aget tmp 0) + update-fn (aget tmp 1)] + (use-memo #js [value] #(State. update-fn value))))) + +(defn use-var + "A rumext custom hook that uses `useRef` under the hood. Returns an + object that implements the Atom protocols. The updates does not + trigger rerender." + ([] (use-var nil)) + ([initial] + (let [ref (useRef nil)] + (when (nil? (.-current ^js ref)) + (let [self (fn [value] + (let [target (unchecked-get ref "current")] + (unchecked-set target "value" value)))] + + (unchecked-set self "value" initial) + (unchecked-set ref "current" self) + (specify! self + c/IDeref + (-deref [this] + (.-value ^js this)) + + c/IReset + (-reset! [this v] + (unchecked-set this "value" v)) + + c/ISwap + (-swap! + ([this f] + (unchecked-set this "value" (f (.-value ^js this)))) + ([this f a] + (unchecked-set this "value" (f (.-value ^js this) a))) + ([this f a b] + (unchecked-set this "value" (f (.-value ^js this) a b))) + ([this f a b xs] + (unchecked-set this "value" (apply f (.-value ^js this) a b xs))))))) + + (.-current ^js ref)))) + +;; --- Other API + +(defn element + "Create a react element. This is a public API for the internal `jsx` + function" + ([klass] + (jsx klass #js {} undefined)) + ([klass props] + (let [props (cond + (object? props) ^js props + (map? props) (util/map->obj props) + :else (throw (ex-info "Unexpected props" {:props props})))] + (jsx klass props undefined)))) + +(def ^function create-element react/createElement) + +(def ^function element? react/isValidElement) + +;; --- Higher-Order Components + +(defn memo + "High order component for memoizing component props. Is a rumext + variant of React.memo what accepts a value comparator + function (instead of props comparator)" + ([component] (react/memo component)) + ([component eq?] + (react/memo component #(util/props-equals? eq? %1 %2)))) + +(def ^function memo' + "A raw variant of React.memo." + react/memo) + +(def ^:private schedule + (or (and (exists? js/window) js/window.requestAnimationFrame) + #(js/setTimeout % 16))) + +(defn deferred + "A higher-order component that just deffers the first render to the next tick" + ([component] (deferred component schedule)) + ([component sfn] + (fnc deferred + {::wrap-props false} + [props] + (let [tmp (useState false) + render? (aget tmp 0) + set-render (aget tmp 1)] + (use-effect (fn [] (^function sfn #(^function set-render true)))) + (when ^boolean render? + [:> component props]))))) + +(defn throttle + "A higher-order component that throttles the rendering" + [component ms] + (fnc throttle + {::wrap-props false} + [props] + (let [tmp (useState props) + state (aget tmp 0) + set-state (aget tmp 1) + + ref (useRef false) + render (useMemo + #(gf/throttle + (fn [v] + (when-not ^boolean (ref-val ref) + (^function set-state v))) + ms) + #js [])] + (useEffect #(^function render props) #js [props]) + (useEffect #(fn [] (set-ref-val! ref true)) #js []) + [:> component state]))) + +(defn check-props + "Utility function to use with `memo'`. + Will check the `props` keys to see if they are equal. + + Usage: + + ```clojure + (mf/defc my-component + {::mf/wrap [#(mf/memo' % (mf/check-props [\"prop1\" \"prop2\"]))]} + [props] + ``` + )" + + ([props] (check-props props =)) + ([props eqfn?] + (fn [np op] + (every? #(eqfn? (unchecked-get np %) + (unchecked-get op %)) + props)))) + +(defn use-debounce + "A rumext custom hook that debounces the value changes" + [ms value] + (let [[state update-fn] (useState value) + update-fn (useMemo #(gf/debounce update-fn ms) #js [ms])] + (useEffect #(update-fn value) #js [value]) + state)) + +(defn use-equal-memo + "A rumext custom hook that preserves object identity through using a + `=` (value equality). Optionally, you can provide your own + function." + ([val] + (let [ref (use-ref nil)] + (when-not (= (ref-val ref) val) + (set-ref-val! ref val)) + (ref-val ref))) + ([eqfn val] + (let [ref (use-ref nil)] + (when-not (eqfn (ref-val ref) val) + (set-ref-val! ref val)) + (ref-val ref)))) + +(def ^function use-deferred + "A lisp-case shorter alias for `useDeferredValue`" + react/useDeferredValue) + +(defn use-previous + "A rumext custom hook that returns a value from previous render" + [value] + (let [ref (use-ref value)] + (use-effect #js [value] #(set-ref-val! ref value)) + (ref-val ref))) + +(defn use-update-ref + "A rumext custom hook that updates the ref value if the value changes" + [value] + (let [ref (use-ref value)] + (use-effect #js [value] #(set-ref-val! ref value)) + ref)) + +(defn use-ref-fn + "A rumext custom hook that returns a stable callback pointer what + calls the interned callback. The interned callback will be + automatically updated on each render if the reference changes and + works as noop if the pointer references to nil value." + [f] + (let [ptr (use-ref nil)] + (use-effect #js [f] #(set-ref-val! ptr f)) + (use-fn (fn [] + (let [f (ref-val ptr) + args (js-arguments)] + (when (some? f) + (.apply f args))))))) + + diff --git a/src/rumext/v2/compiler.clj b/src/rumext/v2/compiler.clj new file mode 100644 index 0000000..c320da2 --- /dev/null +++ b/src/rumext/v2/compiler.clj @@ -0,0 +1,583 @@ +;; TODO: move to .CLJ file + +(ns rumext.v2.compiler + " + Hicada - Hiccup compiler aus dem Allgaeu + + NOTE: The code for has been forked like this: + weavejester/hiccup -> r0man/sablono -> Hicada -> rumext" + (:refer-clojure :exclude [compile]) + (:require + [clojure.core :as c] + [clojure.string :as str] + [rumext.v2 :as-alias mf] + [rumext.v2.normalize :as norm] + [rumext.v2.util :as util]) + (:import + cljs.tagged_literals.JSValue)) + +(declare ^:private compile*) +(declare ^:private compile-map-to-js) +(declare ^:private compile-prop) +(declare ^:private compile-to-js) +(declare ^:private compile-vec-to-js) +(declare ^:private emit-jsx) + +(def ^:dynamic *transform-props-recursive* nil) +(def ^:dynamic *handlers* nil) + +(defn- js-value? + [o] + (instance? JSValue o)) + +(defn- valid-props-type? + [o] + (or (symbol? o) + (js-value? o) + (seq? o) + (nil? o) + (map? o))) + +(def default-handlers + {:> (fn [& [_ tag props :as children]] + (when (> 2 (count children)) + (throw (ex-info "invalid params for `:>` handler, tag and props are mandatory" + {:params children}))) + + (let [props (or props {}) + props (if (instance? clojure.lang.IObj props) + (let [mdata (meta props)] + (vary-meta props assoc + ::handler :> + ::transform-props-keys true + ::transform-props-recursive (get mdata ::mf/recursive false))) + props)] + [tag props (drop 3 children)])) + + :>> (fn [& [_ tag props :as children]] + (when (> 3 (count children)) + (throw (ex-info "invalid params for `:>` handler, tag and props are mandatory" + {:params children}))) + + (let [props (or props {}) + props (if (instance? clojure.lang.IObj props) + (vary-meta props assoc + ::handler :>> + ::transform-props-keys true + ::transform-props-recursive true) + props)] + [tag props (drop 3 children)])) + + :& (fn [& [_ tag props :as children]] + (when (> 2 (count children)) + (throw (ex-info "invalid params for `:&` handler, tag and props are mandatory" + {:params children}))) + + (when-not (valid-props-type? props) + (throw (ex-info "invalid props type: obj, symbol seq or map is allowed" + {:props props}))) + + (let [props (or props {}) + props (vary-meta props assoc + ::handler :& + ::transform-props-keys false + ::transform-props-recursive false + ::allow-dynamic-transform true)] + [tag props (drop 3 children)])) + + :? (fn [& [_ props :as children]] + (if (map? props) + ['rumext.v2/Suspense props (drop 2 children)] + ['rumext.v2/Suspense {} (drop 1 children)])) + + :* (fn [& [_ props :as children]] + (if (map? props) + ['rumext.v2/Fragment props (drop 2 children)] + ['rumext.v2/Fragment {} (drop 1 children)]))}) + +(defn- unevaluated? + "True if the expression has not been evaluated. + - expr is a symbol? OR + - it's something like (foo bar)" + [expr] + (or (symbol? expr) + (and (seq? expr) + (not= (first expr) `quote)))) + +(defn- literal? + "True if x is a literal value that can be rendered as-is." + [x] + (and (not (unevaluated? x)) + (or (not (or (vector? x) (map? x))) + (and (every? literal? x) + (not (keyword? (first x))))))) + +(defn- join-classes + "Join the `classes` with a whitespace." + [classes] + (->> (map #(if (string? %) % (seq %)) classes) + (flatten) + (remove nil?) + (str/join " "))) + +(defn compile-concat + "Compile efficient and performant string concatenation operation" + [params & {:keys [safe?]}] + (let [xform (comp (filter some?) + (if safe? + (map (fn [part] + (if (string? part) + part + (list 'js* "(~{} ?? \"\")" part)))) + (map identity))) + params (into [] xform params)] + + (if (= 1 (count params)) + (first params) + (let [templ (->> (repeat (count params) "~{}") + (interpose "+") + (reduce c/str ""))] + (apply list 'js* templ params))))) + +(defn- compile-join-classes + "Joins strings space separated" + ([] "") + ([x] x) + ([x & xs] + (compile-concat (interpose " " (cons x xs)) :safe? true))) + +(defn- compile-class-attr-value + [value] + (cond + (or (nil? value) + (keyword? value) + (string? value)) + value + + ;; If we know all classes at compile time, we just join them + ;; correctly and return. + (and (or (sequential? value) + (set? value)) + (every? string? value)) + (join-classes value) + + ;; If we don't know all classes at compile time (some classes are + ;; defined on let bindings per example), then we emit a efficient + ;; concatenation code that executes on runtime + (vector? value) + (apply compile-join-classes value) + + :else value)) + +(defmulti compile-form + "Pre-compile certain standard forms, where possible." + (fn [form] + (when (and (seq? form) (symbol? (first form))) + (name (first form))))) + +(defmethod compile-form "do" + [[_ & forms]] + `(do ~@(butlast forms) ~(compile* (last forms)))) + +(defmethod compile-form "array" + [[_ & forms]] + `(cljs.core/array ~@(mapv compile* forms))) + +(defmethod compile-form "let" + [[_ bindings & body]] + `(let ~bindings ~@(butlast body) ~(compile* (last body)))) + +(defmethod compile-form "let*" + [[_ bindings & body]] + `(let* ~bindings ~@(butlast body) ~(compile* (last body)))) + +(defmethod compile-form "letfn*" + [[_ bindings & body]] + `(letfn* ~bindings ~@(butlast body) ~(compile* (last body)))) + +(defmethod compile-form "for" + [[_ bindings body]] + ;; Special optimization: For a simple (for [x xs] ...) we rewrite the for + ;; to a fast reduce outputting a JS array: + (if (== 2 (count bindings)) + (let [[item coll] bindings] + (if (= 'js (:tag (meta coll))) + `(.map ~coll (fn [~item] ~(compile* body))) + `(reduce (fn [out-arr# ~item] + (.push out-arr# ~(compile* body)) + out-arr#) + (cljs.core/array) ~coll))) + ;; Still optimize a little by giving React an array: + (list 'cljs.core/into-array `(for ~bindings ~(compile* body))))) + +(defmethod compile-form "if" + [[_ condition & body]] + `(if ~condition ~@(doall (for [x body] (compile* x))))) + +(defmethod compile-form "when" + [[_ bindings & body]] + `(when ~bindings ~@(doall (for [x body] (compile* x))))) + +(defmethod compile-form "when-some" + [[_ bindings & body]] + `(when-some ~bindings ~@(butlast body) ~(compile* (last body)))) + +(defmethod compile-form "when-let" + [[_ bindings & body]] + `(when-let ~bindings ~@(butlast body) ~(compile* (last body)))) + +(defmethod compile-form "when-first" + [[_ bindings & body]] + `(when-first ~bindings ~@(butlast body) ~(compile* (last body)))) + +(defmethod compile-form "when-not" + [[_ bindings & body]] + `(when-not ~bindings ~@(doall (for [x body] (compile* x))))) + +(defmethod compile-form "if-not" + [[_ bindings & body]] + `(if-not ~bindings ~@(doall (for [x body] (compile* x))))) + +(defmethod compile-form "if-some" + [[_ bindings & body]] + `(if-some ~bindings ~@(doall (for [x body] (compile* x))))) + +(defmethod compile-form "if-let" + [[_ bindings & body]] + `(if-let ~bindings ~@(doall (for [x body] (compile* x))))) + +(defmethod compile-form "letfn" + [[_ bindings & body]] + `(letfn ~bindings ~@(butlast body) ~(compile* (last body)))) + +(defmethod compile-form "fn" + [[_ params & body]] + `(fn ~params ~@(butlast body) ~(compile* (last body)))) + +(defmethod compile-form "case" + [[_ v & cases]] + `(case ~v + ~@(doall (mapcat + (fn [[test hiccup]] + (if hiccup + [test (compile* hiccup)] + [(compile* test)])) + (partition-all 2 cases))))) + +(defmethod compile-form "condp" + [[_ f v & cases]] + `(condp ~f ~v + ~@(doall (mapcat + (fn [[test hiccup]] + (if hiccup + [test (compile* hiccup)] + [(compile* test)])) + (partition-all 2 cases))))) + +(defmethod compile-form "cond" + [[_ & clauses]] + `(cond ~@(doall + (mapcat + (fn [[check expr]] [check (compile* expr)]) + (partition 2 clauses))))) + +(defmethod compile-form :default [expr] expr) + +(defn- compile-element + "Returns an unevaluated form that will render the supplied vector as a HTML element." + [[tag props & children :as element]] + (cond + ;; e.g. [:> Component {:key "xyz", :foo "bar} ch0 ch1] + (contains? *handlers* tag) + (let [f (get *handlers* tag) + [tag props children] (apply f element)] + (emit-jsx tag props (mapv compile* children))) + + ;; e.g. [:span {} x] + (and (literal? tag) (map? props)) + (let [[tag props _] (norm/element [tag props])] + (emit-jsx tag props (mapv compile* children))) + + ;; We could now interpet this as either: + ;; 1. First argument is the attributes (in #js{} provided by the user) OR: + ;; 2. First argument is the first child element. + ;; We assume #2. Always! + (literal? tag) + (compile-element (list* tag {} props children)) + + ;; Problem: [a b c] could be interpreted as: + ;; 1. The coll of ReactNodes [a b c] OR + ;; 2. a is a React Element, b are the props and c is the first child + ;; We default to 1) (handled below) BUT, if b is a map, we know this must be 2) + ;; since a map doesn't make any sense as a ReactNode. + ;; [foo {...} ch0 ch1] NEVER makes sense to interpret as a sequence + (and (vector? element) (map? props)) + (emit-jsx tag props (mapv compile* children)) + + (seq? element) + (seq (mapv compile* element)) + + ;; We have nested children + ;; [[:div "foo"] [:span "foo"]] + :else + (mapv compile* element))) + +(defn- compile* + "Pre-compile data structures" + [content] + (cond + (vector? content) (compile-element content) + (literal? content) content + :else (compile-form content))) + +(defn compile-prop-key + "Compiles a key to a react compatible key (eg: camelCase)" + [k] + (if (or (keyword? k) (symbol? k)) + (util/ident->prop k) + k)) + +(defn compile-prop-inner-key + "Compiles a key to a react compatible key (eg: camelCase)" + [k] + (if (or (keyword? k) (symbol? k)) + (util/ident->key k) + k)) + +(defn- compile-style-value + [m] + (cond + (map? m) + (reduce-kv + (fn [m k v] + (assoc m (compile-prop-key k) v)) + {} m) + ;; React native accepts :style [{:foo-bar ..} other-styles] so camcase those keys: + (vector? m) + (mapv compile-style-value m) + + :else + m)) + +(defn compile-prop-value + [level val] + (cond + (not *transform-props-recursive*) + val + + (map? val) + (->> val + (into {} (map (partial compile-prop (inc level)))) + (compile-map-to-js)) + + (vector? val) + (->> val + (mapv (partial compile-prop-value (inc level))) + (compile-vec-to-js)) + + :else + val)) + +(defn compile-prop + ([prop] (compile-prop 1 prop)) + ([level [key val :as kvpair]] + (let [key (if (= level 1) + (compile-prop-key key) + (compile-prop-inner-key key))] + (cond + (and (= level 1) + (= key "className")) + [key (compile-class-attr-value val)] + + (and (= level 1) + (= key "style")) + [key (-> val + (compile-style-value) + (compile-map-to-js))] + + (and (= level 1) + (= key "htmlFor")) + [key (if (keyword? val) + (name val) + val)] + + :else + [key (compile-prop-value level val)])))) + +(defn compile-kv-to-js + "A internal method helper for compile kv data structures" + [form] + (let [valid-key? #(or (keyword? %) (string? %)) + form (into {} (filter (comp valid-key? key)) form)] + [(->> form + (map (comp name key)) + (map #(-> (str \' % "':~{}"))) + (interpose ",") + (apply str)) + (vec (vals form))])) + +(defn compile-map-to-js + "Compile a statically known map data sturcture, non-recursivelly to js + expression. Mainly used by macros for create js data structures at + compile time." + [form] + (if (map? form) + (if (empty? form) + (list 'js* "{}") + (let [[keys vals] (compile-kv-to-js form)] + (-> (apply list 'js* (str "{" keys "}") vals) + (vary-meta assoc :tag 'object)))) + form)) + +(defn compile-vec-to-js + "Compile a statically known map data sturcture, non-recursivelly to js + expression. Mainly used by macros for create js data structures at + compile time." + [form] + (if (vector? form) + (if (empty? form) + (list 'js* "[]") + (let [template (->> form + (map (constantly "~{}")) + (interpose ",") + (apply str))] + (-> (apply list 'js* (str "[" template "]") form) + (vary-meta assoc :tag 'object)))) + form)) + +(defn compile-props-to-js + "Transform a props map literal to js object props. By default not + recursive." + [props & {:keys [::transform-props-recursive + ::transform-props-keys] + :or {transform-props-recursive false + transform-props-keys true} + :as params}] + + (binding [*transform-props-recursive* transform-props-recursive] + (cond->> props + (true? transform-props-keys) + (into {} (map (partial compile-prop 1))) + + :always + (compile-map-to-js)))) + +(defn compile-coll-to-js + "Transform map or vector to js object or js array. Recursive by + default." + [coll & {:keys [::transform-props-recursive + ::transform-props-keys] + :or {transform-props-recursive true + transform-props-keys true} + :as params}] + (binding [*transform-props-recursive* transform-props-recursive] + (cond + (map? coll) + (->> coll + (into {} (map (partial compile-prop 2))) + (compile-map-to-js)) + + (vector? coll) + (->> coll + (mapv (partial compile-prop-value 2)) + (compile-vec-to-js)) + + :else + (throw (ex-info "only map or vectors allowed" {}))))) + +(defn compile-to-js-spread + [target other compile-prop] + (cond + (and (symbol? target) + (symbol? other)) + (list 'js* "{...~{}, ...~{}}" target other) + + (and (symbol? target) + (map? other)) + (let [[keys vals] (->> other + (into {} (map compile-prop)) + (compile-kv-to-js)) + template (str "{...~{}, " keys "}")] + (apply list 'js* template target vals)) + + (and (map? target) + (symbol? other)) + (let [[keys vals] (->> target + (into {} (map compile-prop)) + (compile-kv-to-js)) + template (str "{" keys ", ...~{}}")] + (apply list 'js* template (concat vals [other]))) + + (and (map? target) + (map? other)) + (compile-map-to-js (->> (merge target other) + (into {} (map compile-prop)))) + + :else + (throw (IllegalArgumentException. "invalid arguments, only symbols or maps allowed")))) + +(defn emit-jsx + "Emits the final react js code" + [tag props children] + (let [tag (cond + (keyword? tag) (name tag) + (string? tag) tag + (symbol? tag) tag + (seq? tag) tag + :else (throw (ex-info "jsx: invalid tag" {:tag tag}))) + + children (into [] (filter some?) children) + mdata (meta props) + jstag? (= (get mdata :tag) 'js)] + + (if (valid-props-type? props) + (if (or (map? props) (nil? props)) + (let [nchild (count children) + props (cond + (= 0 nchild) + (or props {}) + + (= 1 nchild) + (assoc props :children (peek children)) + + :else + (assoc props :children (apply list 'cljs.core/array children))) + + key (:key props) + props (dissoc props :key) + props (compile-props-to-js props mdata)] + + (if key + (if (> nchild 1) + (list 'rumext.v2/jsxs tag props key) + (list 'rumext.v2/jsx tag props key)) + (if (> nchild 1) + (list 'rumext.v2/jsxs tag props) + (list 'rumext.v2/jsx tag props)))) + + (let [props (if (and (::allow-dynamic-transform mdata) (not jstag?)) + (list 'rumext.v2.util/map->obj props) + props) + nchild (count children)] + (cond + (= 0 nchild) + (list 'rumext.v2/create-element tag props) + + (= 1 nchild) + (list 'rumext.v2/create-element tag props (first children)) + + :else + (apply list 'rumext.v2/create-element tag props children)))) + + (throw (ex-info "jsx: invalid props type" {:props props}))))) + +(defn compile + "Arguments: + - content: The hiccup to compile + - handlers: A map to handle special tags. See default-handlers in this namespace. + " + ([content] + (compile content nil)) + ([content handlers] + (binding [*handlers* (merge default-handlers handlers)] + (compile* content)))) diff --git a/src/rumext/normalize.cljc b/src/rumext/v2/normalize.clj similarity index 91% rename from src/rumext/normalize.cljc rename to src/rumext/v2/normalize.clj index 89e89ee..a2e66e7 100644 --- a/src/rumext/normalize.cljc +++ b/src/rumext/v2/normalize.clj @@ -1,19 +1,21 @@ -(ns rumext.normalize - " - Mostly from sablono + hiccup project. - " - (:require - [rumext.util :as util])) +(ns rumext.v2.normalize) + +(defn- element? + "- is x a vector? + AND + - first element is a keyword?" + [x] + (and (vector? x) (keyword? (first x)))) (defn compact-map "Removes all map entries where the value of the entry is empty." [m] (reduce - (fn [m k] - (let [v (get m k)] - (if (empty? v) - (dissoc m k) m))) - m (keys m))) + (fn [m k] + (let [v (get m k)] + (if (empty? v) + (dissoc m k) m))) + m (keys m))) (defn class-name [x] @@ -111,7 +113,7 @@ (string? x) (list x) - (util/element? x) + (element? x) (list x) (and (list? x) @@ -124,7 +126,7 @@ (and (sequential? x) (sequential? (first x)) (not (string? (first x))) - (not (util/element? (first x))) + (not (element? (first x))) (= (count x) 1)) (children (first x)) @@ -153,6 +155,7 @@ [tag (attributes tag-attrs) (children content)]))) + #_(element [:div#foo 'a]) #_(element [:div.a#foo]) #_(element [:h1.b {:className "a"}]) diff --git a/src/rumext/v2/util.cljc b/src/rumext/v2/util.cljc new file mode 100644 index 0000000..601b351 --- /dev/null +++ b/src/rumext/v2/util.cljc @@ -0,0 +1,190 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) 2016-2020 Andrey Antukh + +(ns ^:no-doc rumext.v2.util + "Runtime helpers" + (:require + #?(:cljs [cljs-bean.core :as bean]) + [cuerdas.core :as str] + [malli.core :as m] + [malli.error :as me])) + +(defn ident->key + [nword] + (let [nword (if (string? nword) nword (name nword))] + (cond + (nil? (str/index-of nword "-")) + nword + + (str/starts-with? nword "-") + (-> nword str/camel str/capital) + + :else + (str/camel nword)))) + +(defn ident->prop + "Compiles a keyword or symbol to string using react prop naming + convention" + [nword] + (let [nword (if (string? nword) nword (name nword))] + (cond + (= nword "class") "className" + (= nword "for") "htmlFor" + (str/starts-with? nword "--") nword + (str/starts-with? nword "data-") nword + (str/starts-with? nword "aria-") nword + :else + (ident->key nword)))) + +#?(:cljs + (defn obj->map + [obj] + (let [keys (.keys js/Object obj) + len (alength keys)] + (loop [i 0 + r (transient {})] + (if (< i len) + (let [key (aget keys i)] + (recur (unchecked-inc i) + (assoc! r (keyword key) (unchecked-get obj key)))) + (persistent! r)))))) + +#?(:cljs + (defn plain-object? + ^boolean + [o] + (and (some? o) + (identical? (.getPrototypeOf js/Object o) + (.-prototype js/Object))))) + +#?(:cljs + (defn map->obj + [o] + (cond + (plain-object? o) + o + + (map? o) + (let [m #js {}] + (run! (fn [[k v]] (unchecked-set m (name k) v)) o) + m) + + :else + (throw (ex-info "unable to create obj" {:data o}))))) + +#?(:cljs + (defn map->props + ([o] (map->props o false)) + ([o recursive?] + (if (object? o) + o + (let [level (if (true? recursive?) 1 recursive?)] + (reduce-kv (fn [res k v] + (let [v (if (keyword? v) (name v) v) + k (cond + (string? k) k + (keyword? k) (if (and (int? level) (not= 1 level)) + (ident->key k) + (ident->prop k)) + :else nil)] + + (when (some? k) + (let [v (cond + (and (= k "style") (map? v)) + (map->props v true) + + (and (int? level) (map? v)) + (map->props v (inc level)) + + :else + v)] + (unchecked-set res k v))) + + res)) + #js {} + o)))))) + +#?(:cljs + (defn wrap-props + [props] + (cond + (object? props) (obj->map props) + (map? props) props + (nil? props) {} + :else (throw (ex-info "Unexpected props" {:props props}))))) + +#?(:cljs + (defn props-equals? + [eq? new-props old-props] + (let [old-keys (.keys js/Object old-props) + new-keys (.keys js/Object new-props) + old-keys-len (alength old-keys) + new-keys-len (alength new-keys)] + (if (identical? old-keys-len new-keys-len) + (loop [idx (int 0)] + (if (< idx new-keys-len) + (let [key (aget new-keys idx) + new-val (unchecked-get new-props key) + old-val (unchecked-get old-props key)] + (if ^boolean (eq? new-val old-val) + (recur (inc idx)) + false)) + true)) + false)))) + +#?(:cljs + (defn symbol-for + [v] + (.for js/Symbol v))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; BEANS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +#?(:cljs + (defn prop->key + [k] + (if (string? k) + (-> k str/kebab keyword) + k))) + +#?(:cljs + (defn react-prop->key + [k] + (if (string? k) + (case k + "htmlFor" :for + "className" :class + (-> k str/kebab keyword)) + k))) + +#?(:cljs + (defn- react-key->prop + [x] + (when (simple-keyword? x) + (ident->prop (name x))))) + +#?(:cljs + (defn- key->prop + [x] + (when (keyword? x) + (str/camel (.-fqn ^cljs.core.Keyword x))))) + +#?(:cljs + (defn bean + [o] + (bean/->clj o + :prop->key prop->key + :key->prop key->prop))) + +#?(:cljs + (defn props-bean + "A props specific bean that properly handles react props naming + conventions" + [o] + (bean/->clj o + :prop->key react-prop->key + :key->prop react-key->prop))) diff --git a/src/rumext/v2/validation.cljs b/src/rumext/v2/validation.cljs new file mode 100644 index 0000000..e82ddb8 --- /dev/null +++ b/src/rumext/v2/validation.cljs @@ -0,0 +1,69 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) 2016-2020 Andrey Antukh + +(ns ^:no-doc rumext.v2.validation + "Runtime helpers" + (:require + [cuerdas.core :as str] + [rumext.v2.util :as util] + [malli.core :as m] + [malli.transform :as mt] + [malli.error :as me])) + +(def default-transformer mt/json-transformer) + +(defn process-explain-kv + [prefix result k v] + (let [nm (if (keyword? k) + (name k) + (str k)) + pk (if prefix + (str prefix "." nm) + nm)] + (cond + (and (vector? v) (every? vector? v)) + (let [data (into {} (map-indexed vector) v)] + (reduce-kv (partial process-explain-kv pk) result data)) + + (and (vector? v) (every? map? v)) + (let [gdata (into {} (comp + (map :malli/error) + (map-indexed vector) + (filter second)) + v) + ndata (into {} (comp + (map #(dissoc % :malli/error)) + (map-indexed vector)) + v) + + result (reduce-kv (partial process-explain-kv pk) result gdata) + result (reduce-kv (partial process-explain-kv pk) result ndata)] + + result) + + (and (vector? v) (every? string? v)) + (assoc result pk (peek v)) + + (map? v) + (reduce-kv (partial process-explain-kv pk) result v) + + :else + result))) + +(defn ^:no-doc validator + [schema] + (let [validator (delay (m/validator schema)) + explainer (delay (m/explainer schema)) + decoder (delay (m/decoder schema default-transformer))] + (fn [props] + (let [props (util/props-bean props) + props (@decoder props) + validate (deref validator)] + (when-not ^boolean (^function validate props) + (let [explainer (deref explainer) + explain (^function explainer props) + explain (me/humanize explain)] + (reduce-kv (partial process-explain-kv nil) {} explain))))))) diff --git a/yarn.lock b/yarn.lock index 0d4e11f..b367e11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,698 +1,161 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - -base64-js@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: - version "4.11.9" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== - -bn.js@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" - integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== - -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" - integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= - dependencies: - bn.js "^4.1.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -elliptic@^6.5.3: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== - dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" - hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" - -events@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" - integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - -ieee754@^1.1.4: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -"js-tokens@^3.0.0 || ^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -loose-envify@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -node-libs-browser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -pbkdf2@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" - integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -react-dom@17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6" - integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.1" - -react@17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" - integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -readable-stream@^2.0.2, readable-stream@^2.3.3, readable-stream@^2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readline-sync@^1.4.7: - version "1.4.10" - resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" - integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw== - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safer-buffer@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -scheduler@^0.20.1: - version "0.20.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c" - integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shadow-cljs-jar@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" - integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== - -shadow-cljs@2.11.8: - version "2.11.8" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.11.8.tgz#34f579a96f90f79f6fac46ff901d81695c2ea0c0" - integrity sha512-8k2t6lLHDseWTcqizkIyJNVInYTYcd7v8uEE3CWYrlqlNZ+U3TQ4FsUS2pRXUfosNgvdkM7hw61pvwRk+KB5TA== - dependencies: - node-libs-browser "^2.2.1" - readline-sync "^1.4.7" - shadow-cljs-jar "1.3.2" - source-map-support "^0.4.15" - which "^1.3.1" - ws "^3.0.0" - -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -string_decoder@^1.0.0, string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -timers-browserify@^2.0.4: - version "2.0.11" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" - integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ== - dependencies: - setimmediate "^1.0.4" - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -ultron@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" - integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - -vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -ws@^3.0.0: - version "3.3.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" - integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA== - dependencies: - async-limiter "~1.0.0" - safe-buffer "~5.1.0" - ultron "~1.1.0" - -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf + languageName: node + linkType: hard + +"buffer-from@npm:^1.0.0": + version: 1.1.2 + resolution: "buffer-from@npm:1.1.2" + checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 + languageName: node + linkType: hard + +"buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0 + languageName: node + linkType: hard + +"ieee754@npm:^1.2.1": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + +"process@npm:^0.11.10": + version: 0.11.10 + resolution: "process@npm:0.11.10" + checksum: 10c0/40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3 + languageName: node + linkType: hard + +"react-dom@npm:19.1.0": + version: 19.1.0 + resolution: "react-dom@npm:19.1.0" + dependencies: + scheduler: "npm:^0.26.0" + peerDependencies: + react: ^19.1.0 + checksum: 10c0/3e26e89bb6c67c9a6aa86cb888c7a7f8258f2e347a6d2a15299c17eb16e04c19194e3452bc3255bd34000a61e45e2cb51e46292392340432f133e5a5d2dfb5fc + languageName: node + linkType: hard + +"react@npm:19.1.0": + version: 19.1.0 + resolution: "react@npm:19.1.0" + checksum: 10c0/530fb9a62237d54137a13d2cfb67a7db6a2156faed43eecc423f4713d9b20c6f2728b026b45e28fcd72e8eadb9e9ed4b089e99f5e295d2f0ad3134251bdd3698 + languageName: node + linkType: hard + +"readline-sync@npm:^1.4.10": + version: 1.4.10 + resolution: "readline-sync@npm:1.4.10" + checksum: 10c0/0a4d0fe4ad501f8f005a3c9cbf3cc0ae6ca2ced93e9a1c7c46f226bdfcb6ef5d3f437ae7e9d2e1098ee13524a3739c830e4c8dbc7f543a693eecd293e41093a3 + languageName: node + linkType: hard + +"rumext@workspace:.": + version: 0.0.0-use.local + resolution: "rumext@workspace:." + dependencies: + process: "npm:^0.11.10" + react: "npm:19.1.0" + react-dom: "npm:19.1.0" + shadow-cljs: "npm:3.1.3" + languageName: unknown + linkType: soft + +"scheduler@npm:^0.26.0": + version: 0.26.0 + resolution: "scheduler@npm:0.26.0" + checksum: 10c0/5b8d5bfddaae3513410eda54f2268e98a376a429931921a81b5c3a2873aab7ca4d775a8caac5498f8cbc7d0daeab947cf923dbd8e215d61671f9f4e392d34356 + languageName: node + linkType: hard + +"shadow-cljs-jar@npm:1.3.4": + version: 1.3.4 + resolution: "shadow-cljs-jar@npm:1.3.4" + checksum: 10c0/c5548bb5f2bda5e0a90df6f42e4ec3a07ed4c72cdebb87619e8d9a2167bb3d4b60d6f6a305a3e15cbfb379d5fdbe2a989a0e7059b667cfb3911bc198a4489e94 + languageName: node + linkType: hard + +"shadow-cljs@npm:3.1.3": + version: 3.1.3 + resolution: "shadow-cljs@npm:3.1.3" + dependencies: + buffer: "npm:^6.0.3" + process: "npm:^0.11.10" + readline-sync: "npm:^1.4.10" + shadow-cljs-jar: "npm:1.3.4" + source-map-support: "npm:^0.5.21" + which: "npm:^5.0.0" + ws: "npm:^8.18.1" + bin: + shadow-cljs: cli/runner.js + checksum: 10c0/aee011854e0646b7b6f483b47cba573263477cd5b39bd5edb35830233cd7f6c2db4d98a629cdf003f81f9e4818f81fa00b5ccfa684dbf74156889c93ec80a666 + languageName: node + linkType: hard + +"source-map-support@npm:^0.5.21": + version: 0.5.21 + resolution: "source-map-support@npm:0.5.21" + dependencies: + buffer-from: "npm:^1.0.0" + source-map: "npm:^0.6.0" + checksum: 10c0/9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d + languageName: node + linkType: hard + +"source-map@npm:^0.6.0": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + +"which@npm:^5.0.0": + version: 5.0.0 + resolution: "which@npm:5.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 10c0/e556e4cd8b7dbf5df52408c9a9dd5ac6518c8c5267c8953f5b0564073c66ed5bf9503b14d876d0e9c7844d4db9725fb0dcf45d6e911e17e26ab363dc3965ae7b + languageName: node + linkType: hard + +"ws@npm:^8.18.1": + version: 8.18.2 + resolution: "ws@npm:8.18.2" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/4b50f67931b8c6943c893f59c524f0e4905bbd183016cfb0f2b8653aa7f28dad4e456b9d99d285bbb67cca4fedd9ce90dfdfaa82b898a11414ebd66ee99141e4 + languageName: node + linkType: hard