diff --git a/.gitignore b/.gitignore index 21f823ef3..734b66391 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,5 @@ coverage/ types/ packages/solid/src/jsx.d.ts -packages/solid/web/server-async/asyncSSR.d.ts -packages/solid/web/server/syncSSR.d.ts -packages/solid/web/src/runtime.d.ts \ No newline at end of file +packages/solid/web/server/server.d.ts +packages/solid/web/src/client.d.ts \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7153a6ad1..2ca2309d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,69 @@ # Changelog +## 0.24.0 - 2021-02-03 + +This release is the start of the rework of the SSR solution. Consolidating them under a single method. Unfortunately this one comes with several breaking changes. + +### Breaking Changes + +#### Removed `solid-js/dom` + +It's been a few versions deprecated. It's gone. + +#### Updated Resource API + +Changed to more resemble SWR and React Query. Needed to remove `createResourceState`so now need to use a getter over `createResource` to get same effect. See updated documentation. + +#### Change SSR render call signatures + +They now return results objects that include the generated hydration script. No more need to generate it separately. Also comes autowrapped in the `script` tag now. + +#### `assignProps` to `mergeProps` + +While you use them the same way mostly it no longer has `Object.assign` semantics and always returns a new object. This is important as in many cases we need to upgrade to a Proxy. + +#### Renamed `getContextOwner` to `getOwner` + +Removes confusion around context and consistent with new helper `runWithOwner`. + +#### Solid Element no longer uses State for props + +This reduces the size of the library especially for those not using state. It also should slightly increase performance as no need for deep nesting of proxies. It also makes things behave more consistently avoided unintended deep wrapping. + +### Non-breaking Changes + +#### New non-reactive Async SSR + +I have now combined sync/streaming/async SSR into the same compiler output. To do so I have developed a new non-reactive Async SSR approach. After realizing how fast Solid renders, it occurred to me on the server we could do a much simpler approach if we were willing to re-render all content in Suspense boundaries. While that is some wasted work, compared to including the reactive system it's a killing. + +#### Increase SSR Performance + +Through reusing static strings in the template we reduce repeated creation costs. This small improvement can make 5-8% improvements where you have many rows. + +#### Event Delegation + +Solid is now being more strict on what events it delegates. Limiting to standard pointer/touch/mouse/keyboard events. Custom events will no longer be delegated automatically. This increases compatibility for Web Component users who don't compose their events. Non-delegated events will still work and binding array syntax with them. + +#### State getters no longer memos + +Automatic memos put some constraints on the disposal system that get in the way of making the approach flexible to hold all manner of reactive primitives. Some previous limitations included not being able to have nested getters. You can still manually create a memo and put it in a getter but the default will not be memoized. +### New Features + +#### `children` helper + +Resolves children and returns a memo. This makes it much easier to deal with children. Using same mechanism `` can now have dynamic children like `` inside. + +#### "solid" Export Conidition +This is the way to package the JSX components to be compiled to work on server or client. By putting the "solid" condition the source JSX will be prioritized over normal browser builds. + +### Bug Fixes + +* Top level primitive values not working with `reconcile` +* Fix Dynamic Components to handle SVG +* Rename potentially conflicting properties for event delegtion +* Fixed State spreads to not loose reactiviy. Added support for dynamically created properties to track in spreads and helpers +* TypeScript, always TypeScript + ## 0.23.0 - 2020-12-05 This release is mostly bug fixes. Breaking change for TS users. JSX types no longer pollutes global namespace. This means you need to update your projects to import it. diff --git a/documentation/api.md b/documentation/api.md index f8b810b21..20d57a43b 100644 --- a/documentation/api.md +++ b/documentation/api.md @@ -4,9 +4,13 @@ This is the smallest and most primitive reactive atom used to track a single value. By default signals always notify on setting a value. You can have it only notify on changes if you pass true to the second parameter. Or a custom comparator can be passed in to indicate whether the values should be considered equal and listeners not notified. -### `createState(initValue): [state, setState]` +### `createMemo(prev => , initialValue, boolean | comparatorFn): getValueFn` -Creates a new State proxy object and setState pair. State only triggers update on values changing. Tracking is done by intercepting property access and automatically tracks deep nesting via proxy. +Creates a readonly derived signal that recalculates it's value whenever the executed codes dependencies update. By default memos always notify on updating a value. You can have it only notify on changes if you pass true to the second parameter. Or a custom comparator can be passed in to indicate whether the values should be considered equal and listeners not notified. + +### `createEffect(prev => , initialValue): void` + +Creates a new computation that automatically tracks dependencies and runs after each render where a dependency has changed. Ideal for using `ref`s and managing other side effects. 2nd argument is the initial value. ### `onMount(() => )` @@ -16,17 +20,9 @@ Registers a method that runs after initial render and elements have been mounted Registers a cleanup method that executes on disposal or recalculation of the current context. Can be used in components or computations. -### `createMemo(prev => , initialValue, boolean | comparatorFn): getValueFn` - -Creates a readonly signal that recalculates it's value whenever the executed codes dependencies update. By default memos always notify on updating a value. You can have it only notify on changes if you pass true to the second parameter. Or a custom comparator can be passed in to indicate whether the values should be considered equal and listeners not notified. - -### `createComputed(prev => , initialValue): void` - -Creates a new computation that automatically tracks dependencies and runs immediately. Use this to write to other reactive primitives or to reactively trigger async data loading. 2nd argument is the initial value. - -### `createEffect(prev => , initialValue): void` +### `createState(initValue): [state, setState]` -Creates a new computation that automatically tracks dependencies and runs after each render where a dependency has changed. Ideal for using `ref`s and managing other side effects. 2nd argument is the initial value. +Creates a new State proxy object and setState pair. State only triggers update on values changing. Tracking is done by intercepting property access and automatically tracks deep nesting via proxy. ### `createContext(defaultContext): Context` @@ -68,6 +64,10 @@ Creates a new mutable State proxy object. State only triggers update on values c Creates memo that only notifies downstream changes when the browser is idle. `timeoutMs` is the maximum time to wait before forcing the update. +### `createComputed(prev => , initialValue): void` + +Creates a new computation that automatically tracks dependencies and runs immediately. Use this to write to other reactive primitives or to reactively trigger async data loading before render. 2nd argument is the initial value. + ### `createRenderEffect(prev => , initialValue): void` Creates a new computation that automatically tracks dependencies and runs during the render phase as DOM elements are created and updated but not necessarily connected. All internal DOM updates happen at this time. @@ -76,13 +76,9 @@ Creates a new computation that automatically tracks dependencies and runs during Creates a conditional signal that only notifies subscribers when entering or exiting their key matching the value. Useful for delegated selection state. -### `createResource(initialValue, options: { name }): [getValueFn, loadFn]` - -Creates a new resource signal that can hold an async resource. Resources when read while loading trigger Suspense. The `loadFn` takes a Promise whose resolved value is set in the resource. - -### `createResourceState(initialValue, options: { name }): [state, loadState, setState]` +### `createResource(key, fetcher, initialValue): getValueFn` -Creates a new Resource State object. Similar to normal state except each immediate property is a resource. +Creates a new resource signal that can hold an async resource. Resources when read while loading trigger Suspense. The `fetcher` is a function that accepts key and returns a Promise whose resolved value is set in the resource. ### `lazy(() => ): Component` @@ -92,9 +88,9 @@ Used to lazy load components to allow for things like code splitting and Suspens Used to batch async updates deferring commit until all async processes are complete. -### `assignProps(target, ...sources): target` +### `mergeProps(...sources): target` -A reactive object `assign` method. Useful for setting default props for components in case caller doesn't provide them. Or cloning the props object including reactive properties. +A reactive object `merge` method. Useful for setting default props for components in case caller doesn't provide them. Or cloning the props object including reactive properties. ### `splitProps(props, ...keyArrays): [...splitProps]` diff --git a/documentation/components.md b/documentation/components.md index 91d7ca05f..05b763023 100644 --- a/documentation/components.md +++ b/documentation/components.md @@ -84,13 +84,13 @@ To help maintain reactivity Solid has a couple prop helpers: ```jsx // default props -props = assignProps({}, { name: "Smith" }, props); +props = mergeProps({ name: "Smith" }, props); // clone props -const newProps = assignProps({}, props); +const newProps = mergeProps(props); // merge props -assignProps(props, otherProps); +props = mergeProps(props, otherProps); // split props into multiple props objects const [local, others] = splitProps(props, ["className"]) diff --git a/documentation/reactivity.md b/documentation/reactivity.md index f0d2f5b26..3e542919e 100644 --- a/documentation/reactivity.md +++ b/documentation/reactivity.md @@ -2,9 +2,9 @@ Solid's data management is built off a set of flexible reactive primitives which are responsible for all the updates. It takes a very similar approach to MobX or Vue except it never trades its granularity for a VDOM. Dependencies are automatically tracked when you access your reactive values in your Effects and JSX View code. -Solid has a number of reactive primitives but the main 2 are Signals, and State. Ultimately you will need to understand both to write effective Solid code. +Solid's primitives come in the form of `create` calls that often return tuples, where generally the first element is a readable primitive and the second is a setter. It is common to refer to only the readable part by the primitive name. -Signals hold simple values that you view as atomic immutable cells that consist of a getter and setter. These are ideal for simple local component values. They are called signals as they act as tiny streams that wire your application together. +Here is a basic auto incrementing counter that is updating based on setting the `count` signal. ```jsx import { createSignal, onCleanup } from "solid-js"; @@ -21,49 +21,16 @@ const App = () => { render(() => , document.getElementById("app")); ``` -> **For React Users:** This looks like React Hooks, but it is very different. There are no Hook rules, or concern about stale closures because your Component only runs once. It is only the "Hooks" that re-execute. So they always have the latest. - -Solid's state object are deeply nested reactive data trees useful for global stores, model caches, and 3rd party immutable data interopt. They have a much more powerful setter that allows to specify nested changes and use value and function forms for updates. - -They can be used in Components as well and is the go to choice when data gets more complicated (nested). - -```jsx -import { createState, onCleanup } from "solid-js"; -import { render } from "solid-js/web"; - -const App = () => { - const [state, setState] = createState({ - user: { - firstName: "John", - lastName: "Smith", - get fullName() { - return `${this.firstName} ${this.lastName}`; - } - } - }); - - return ( -
setState("user", "lastName", value => value + "!")}> - {state.user.fullName} -
- ); -}; - -render(() => , document.getElementById("app")); -``` - -Remember if you destructure or spread a state object reactivity is lost. However, unlike Vue we don't separate our `setup` from our view code so there is little concern about transforming or transfering these reactive atoms around. Just access the properties where you need them. - -With Solid State and Context API you really don't need 3rd party global stores. These proxies are optimized part of the reactive system and lend to creating controlled unidirectional patterns. +> **For React Users:** This looks like React Hooks, but it is very different. There are no Hook rules, or concern about stale closures because your Component only runs once. It is only the "Hooks" that re-execute. So they always have the latest references. ## Signals -Signals are the glue that hold the library together. They are a simple primitive that contain values that change over time. With Signals you can track all sorts of changes from various sources in your applications. You can update a Signal manually or from any Async source. +Signals are the glue that hold the library together. They are a simple primitive that contain values that change over time. With Signals you can track all sorts of changes from various sources in your applications. They are not tied to any specific component and can be used wherever whenever. ```js import { createSignal, onCleanup } from "solid-js"; -function useTick(delay) { +function createTick(delay) { const [getCount, setCount] = createSignal(0), handle = setInterval(() => setCount(getCount() + 1), delay); onCleanup(() => clearInterval(handle)); @@ -71,26 +38,28 @@ function useTick(delay) { } ``` -## Accessors Reactive Scope +## Reactive Scope and Tracking Signals are special functions that when executed return their value. In addition they are trackable when executed under a reactive scope. This means that when their value is read (executed) the currently executing reactive scope is now subscribed to the Signal and will re-execute whenever the Signal is updated. -This mechanism is based on the executing function's scope so Signals reads can be composed and nested as many levels as desired. By wrapping a Signal read in a thunk `() => signal()` you have effectively created a derived signal that can be tracked as well. The same holds true for accessing state. Want to use state as a signal just wrap it in a function: +This method of tracking wraps the execution stack so Signals can be accessed any number of levels deep. In so, by wrapping a Signal read in a thunk `() => signal()` you have effectively created a derived signal that can be tracked as well. The same holds true for accessing props or Solid's reactive State proxies. Want to use state as a signal just wrap it in a function: ```js -// I can be tracked +// I can be tracked later const firstName = () => state.user.firstName; return
{firstName()}
; ``` -These accessors are just functions that can be tracked and return a value. No additional primitive or method is needed for them to work as Signals in their own right. However, you need another primitive to create that reactive scope: +These are just functions that can be tracked and return a value. No additional primitive or method is needed for them to work as Signals in their own right. This is because Signals are readonly. Any pure function that wraps a signal is also a Signal. + +However, you need another primitive to actually execute the work and track the these signals. ## Computations A computation is calculation over a function execution that automatically and dynamically tracks any child signals that are accessed during that execution. A computation goes through a cycle on execution where it releases its previous execution's dependencies, then executes grabbing the current dependencies. -There are 3 main computations used by Solid: Memos which are pure and designed to cache values until their reactivity forces re-evaluation, Computeds which are designed to write to other signals, and Effects which are intended to produce side effects after rendering. +There are 2 main types of computations. Those that are pure and meant to derive a value called Memos and those that update the outside world and produce side effects, aptly called Effects. ```js import { createSignal, createEffect, createMemo } from "solid-js"; @@ -106,7 +75,7 @@ setCount(count() + 1); Effects are what allow the DOM to stay up to date. While you don't see them, everytime you write an expression in the JSX(code between the parenthesis `{}`), the compiler is wrapping it in a function and passing it to a `createEffect` call. -Memos allow us to store and access values without re-evaluating them until their dependencies change. +Memos allow us to store and access values without re-evaluating them until their dependencies change. They are very similar to derived Signals mentioned above except they only re-evaluate when their dependencies change and return the last cached value on read. Keep in mind memos are only necessary if you wish to prevent re-evaluation when the value is read. Useful for expensive operations like DOM Node creation. Any example with a memo could also just be a function and effectively be the same without caching as it's just another signal. @@ -126,6 +95,7 @@ setCount(count() + 1); Memos also pass the previous value on each execution. This is useful for reducing operations (obligatory Redux in a couple lines example): ```js +// reducer const reducer = (state, action = {}) => { switch (action.type) { case "LIST/ADD": @@ -135,9 +105,12 @@ const reducer = (state, action = {}) => { } }; +// initial state +const state = { list: [] }; + // redux const [getAction, dispatch] = createSignal(), - getStore = createMemo(state => reducer(state, getAction()), { list: [] }); + getStore = createMemo(state => reducer(state, getAction()), state); // subscribe and dispatch createEffect(() => console.log(getStore().list)); @@ -171,13 +144,6 @@ For convenience Solid provides an `on` operator to set up explict dependencies f createEffect(on(a, v => console.log(v, b()))); ``` -Another situation is maybe you want something to run only on mount: - -```js -// does not update when props update -createEffect(() => untrack(() => console.log("Mounted with", props.a, props.b))); -``` - Solid executes synchronously but sometimes you want to apply multiple changes at once. `batch` allows us to do that without triggering updates multiple times. ```js @@ -197,10 +163,10 @@ _Note: Solid's graph is synchronously executed so any starting point that isn't ## Composition -State and Signals combine wonderfully as wrapping a state selector in a function instantly makes it reactive accessor. They encourage composing more sophisticated patterns to fit developer need. +Solid's primitives combine wonderfully. They encourage composing more sophisticated patterns to fit developer need. ```js -// deep reconciled immutable reducer +// Solid's fine-grained exivalent to React's `useReducer` Hook const useReducer = (reducer, init) => { const [state, setState] = createState(init), [getAction, dispatch] = createSignal(); diff --git a/documentation/state.md b/documentation/state.md index b4e335f74..9d94f7f3b 100644 --- a/documentation/state.md +++ b/documentation/state.md @@ -10,20 +10,38 @@ Through the use of proxies and explicit setters it gives the control of an immut ### `createState(object)` -Initializes with object value and returns an array where the first index is the state object and the second is the setState method. +Solid's state object are deeply nested reactive data trees useful for global stores, model caches, and 3rd party immutable data interopt. They have a much more powerful setter that allows to specify nested changes and use value and function forms for updates. -Initial state consists of a tree of values, including getters that are automatically wrapped in a Memo that can define derived values: +They can be used in Components as well and is the go to choice when data gets more complicated (nested). ```jsx -const [state, setState] = createState({ - firstName: "John", - lastName: "Miller", - get fullName() { - return `${this.firstName} ${this.lastName}`; - } -}); +import { createState } from "solid-js"; +import { render } from "solid-js/web"; + +const App = () => { + const [state, setState] = createState({ + user: { + firstName: "John", + lastName: "Smith", + get fullName() { + return `${this.firstName} ${this.lastName}`; + } + } + }); + + return ( +
setState("user", "lastName", value => value + "!")}> + {state.user.fullName} +
+ ); +}; + +render(() => , document.getElementById("app")); ``` -> Note: getters are only currently supported top level + +Remember if you destructure or spread a state object outside of a computation or JSX reactivity is lost. However, unlike Vue we don't separate our `setup` from our view code so there is little concern about transforming or transfering these reactive atoms around. Just access the properties where you need them. + +With Solid State and Context API you really don't need 3rd party global stores. These proxies are optimized part of the reactive system and lend to creating controlled unidirectional patterns. ### `setState(changes)` diff --git a/documentation/suspense.md b/documentation/suspense.md index dd157e679..1547919cb 100644 --- a/documentation/suspense.md +++ b/documentation/suspense.md @@ -164,20 +164,16 @@ const App = () => { ## Data Loading -Solid ships with two resource containers to handle async loading. One is a signal created by `createResource` and the other a state object created by `createResourceState`. The signal is a simple reactive atom so it's reactivity is not deeply nested. Whereas state deeply nests reactive properties. - -Both have trackable `loading` property. On the signal it's a boolean. On the state object it is an object with a boolean per key. +Solid ships with a primitive to handle async data loading, `createResource`. It has a trackable `loading` property. ```jsx import { createResource } from "solid-js"; // notice returns a function that returns a promise -const fetchUser = id => - () => fetch(`https://swapi.co/api/people/${id}/`).then(r => r.json()); +const fetchJSON = query => fetch(query).then(r => r.json()); export default const UserPanel = props => { - let [user, load] = createResource(); - load(fetchUser(props.userId))); + let [user] = createResource(() => `https://swapi.co/api/people/${props.userId}/`, fetchJSON); return
@@ -195,35 +191,16 @@ export default const UserPanel = props => { } ``` -```jsx -import { createResourceState } from "solid-js"; - - -// notice returns a function that returns a promise -const fetchUser = id => - () => fetch(`https://swapi.co/api/people/${id}/`).then(r => r.json()); +This example handles the different loading states. However, you can expand this example to use Suspense instead by wrapping with the `Suspense` Component. -export default const UserPanel = props => { - let [state, load] = createResourceState(); - load({ user: fetchUser(props.userId) }); +Resource also returns actions that can be performed, like `refetch` and `mutate`. And if absent using `fetch` to return JSON is the default fetcher. - return
- - Loading... - { user => -

{user.name}

-
    -
  • Height: {user.height}
  • -
  • Mass: {user.mass}
  • -
  • Birth Year: {user.birthYear}
  • -
- }
-
-
-} +```js +let [user, { refetch, mutate }] = createResource( + () => `https://swapi.co/api/people/${props.userId}/` +); ``` - -These examples handle the different loading states. However, you can expand this example to use Suspense instead by wrapping with the `Suspense` Component. +The first argument can be a unique string key or dynamically generated one that is automatically tracked in a function. > **For React Users:** At the time of writing this React has not completely settled how their Data Fetching API will look. Solid ships with this feature today, and it might differ from what React ultimately lands on. @@ -233,8 +210,8 @@ It is important to note that Suspense is tracked based on data requirements of t ```jsx // start loading data before any part of the page is executed. -const [state, load] = createResourceState(); -load({ user: fetchUser(), posts: fetchPosts() }); +const [user] = createResource("user", fetchUser); +const [posts] = createResource("posts", fetchPost); function ProfilePage() { return ( diff --git a/lerna.json b/lerna.json index da9201583..97b155cc3 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": [ "packages/*" ], - "version": "0.23.11" + "version": "0.24.0-beta.4" } diff --git a/package-lock.json b/package-lock.json index 4acd5c8eb..084bc1ada 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,51 @@ { "name": "solid-js", - "version": "0.23.0", + "version": "0.24.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/cli": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.12.10.tgz", + "integrity": "sha512-+y4ZnePpvWs1fc/LhZRTHkTesbXkyBYuOB+5CyodZqrEuETXi3zOVfpAQIdgC3lXbHLTDG9dQosxR9BhvLKDLQ==", + "dev": true, + "requires": { + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents", + "chokidar": "^3.4.0", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.0.0", + "lodash": "^4.17.19", + "make-dir": "^2.1.0", + "slash": "^2.0.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", @@ -792,12 +834,20 @@ } }, "@babel/plugin-syntax-jsx": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", - "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.13.tgz", + "integrity": "sha512-d4HM23Q1K7oq/SLNmG6mRt85l2csmQ0cHRaxRXjKW0YFdEXqlZ5kzFQKH5Uc3rDJECgu+yCRgPkG04Mm98R/1g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz", + "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==", + "dev": true + } } }, "@babel/plugin-syntax-logical-assignment-operators": { @@ -3529,6 +3579,185 @@ "glob-to-regexp": "^0.3.0" } }, + "@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz", + "integrity": "sha512-+nb9vWloHNNMFHjGofEam3wopE3m1yuambrrd/fnPc+lFOMB9ROTqQlche9ByFWNkdNqfSgR/kkQtQ8DzEWt2w==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "optional": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -4277,6 +4506,13 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true, + "optional": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4431,9 +4667,9 @@ } }, "babel-plugin-jsx-dom-expressions": { - "version": "0.24.7", - "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.24.7.tgz", - "integrity": "sha512-WaBX9z81sL0NDRVjeXr1GhJPkIMbz9GiT42/I6RqouN4y4kf6hEaARbG7NMk6YKOmc31o64fb0mYo3gj78nYmQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.25.0.tgz", + "integrity": "sha512-U6ET1eUlQyNYJKKUcNgjEGM/H5xkMKscBdFGFBTW++vy5xq0oxILPuxrMtYmS6orA1/Imd6nV5zR7g95+uVCEg==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.10.4", @@ -4562,6 +4798,13 @@ "is-windows": "^1.0.0" } }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "optional": true + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -4801,6 +5044,59 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "dependencies": { + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "optional": true + }, + "fsevents": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", + "integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", + "dev": true, + "optional": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "optional": true, + "requires": { + "picomatch": "^2.2.1" + } + } + } + }, "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -5875,9 +6171,9 @@ } }, "dom-expressions": { - "version": "0.24.7", - "resolved": "https://registry.npmjs.org/dom-expressions/-/dom-expressions-0.24.7.tgz", - "integrity": "sha512-A0CADsk23P7LA/ITZi6ac/LLr1Lcd6aUpEudO/8gmig1CPyl3NOzVb2+aSum/klZY5kxm7mnqtPBk+EQerGN1A==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/dom-expressions/-/dom-expressions-0.25.0.tgz", + "integrity": "sha512-nZRYqrqLVcc9mKyWK4URZakw+L7zI/HWOwvcXlZQwVNoRVXQYD16rBd5IsnpK1fUPA5WECxU5pFa3G6W10D4yA==", "dev": true, "requires": { "babel-plugin-transform-rename-import": "^2.3.0" @@ -6682,6 +6978,12 @@ "minipass": "^2.6.0" } }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -7754,9 +8056,9 @@ } }, "hyper-dom-expressions": { - "version": "0.24.7", - "resolved": "https://registry.npmjs.org/hyper-dom-expressions/-/hyper-dom-expressions-0.24.7.tgz", - "integrity": "sha512-U//Zs/SPO6cLhsVT41vSR6DnLAOgnjogplZ74urK4NQZL9BHbBJ+ema7ru09lp8hPFP5bp7Z/sIe4/vMvvFa4w==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/hyper-dom-expressions/-/hyper-dom-expressions-0.25.0.tgz", + "integrity": "sha512-cVQ0DsMW5oE03/2gxgsEUT2vYblRSOEdAKxnVMFwAG3xMc3HLWlBQN4BgOA97Br037+M8ktMsYVUwDBUrr+RVg==", "dev": true }, "iconv-lite": { @@ -7990,6 +8292,16 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -10917,9 +11229,9 @@ "dev": true }, "lit-dom-expressions": { - "version": "0.24.7", - "resolved": "https://registry.npmjs.org/lit-dom-expressions/-/lit-dom-expressions-0.24.7.tgz", - "integrity": "sha512-lVeKCMX6Gaqzx3PUEEVpaaT20l3okBLphDZ8/6DFONWMVZzg50OrMH9fOcixM3aub2tDgFvgWRpULcwbxDqNZg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/lit-dom-expressions/-/lit-dom-expressions-0.25.0.tgz", + "integrity": "sha512-v6KsKyD1HW01GV6sWsyn0ilod2eSf25uZ6UfB27Wr1Vwcc2SPw4ITQYXql0O9BsSK63MRDJLX1AiDoHY+18lOA==", "dev": true }, "load-json-file": { @@ -12485,6 +12797,131 @@ "once": "^1.3.0" } }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "optional": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, "redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", diff --git a/package.json b/package.json index aa5f4e8b3..a531cd770 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "solid-js", "description": "A declarative JavaScript library for building user interfaces.", - "version": "0.23.0", + "version": "0.24.0", "author": "Ryan Carniato", "license": "MIT", "repository": { @@ -22,6 +22,7 @@ "report:coverage": "lerna run report:coverage --parallel" }, "devDependencies": { + "@babel/cli": "^7.12.10", "@babel/core": "^7.12.9", "@babel/preset-env": "^7.12.7", "@babel/preset-typescript": "^7.12.7", @@ -32,14 +33,14 @@ "@rollup/plugin-replace": "2.3.3", "@types/jest": "^26.0.14", "babel-jest": "^26.6.3", - "babel-plugin-jsx-dom-expressions": "~0.24.7", + "babel-plugin-jsx-dom-expressions": "~0.25.0", "coveralls": "^3.1.0", - "dom-expressions": "0.24.7", - "hyper-dom-expressions": "0.24.7", + "dom-expressions": "0.25.0", + "hyper-dom-expressions": "0.25.0", "jest": "~26.6.3", "jest-ts-webcompat-resolver": "^1.0.0", "lerna": "^3.22.1", - "lit-dom-expressions": "0.24.7", + "lit-dom-expressions": "0.25.0", "ncp": "2.0.0", "npm-run-all": "^4.1.5", "rimraf": "^3.0.2", diff --git a/packages/babel-preset-solid/index.js b/packages/babel-preset-solid/index.js index b3e1e675c..4ed9486e6 100644 --- a/packages/babel-preset-solid/index.js +++ b/packages/babel-preset-solid/index.js @@ -19,10 +19,8 @@ module.exports = function (context, options = {}) { "Dynamic", "ErrorBoundary" ], - delegateEvents: true, contextToCustomElements: true, wrapConditionals: true, - wrapSpreads: false, generate: "dom" }, options diff --git a/packages/babel-preset-solid/package.json b/packages/babel-preset-solid/package.json index c8df29d19..94122fae9 100644 --- a/packages/babel-preset-solid/package.json +++ b/packages/babel-preset-solid/package.json @@ -1,6 +1,6 @@ { "name": "babel-preset-solid", - "version": "0.23.8", + "version": "0.24.0-beta.3", "description": "Babel preset to transform JSX for Solid.js", "author": "Ryan Carniato ", "homepage": "https://github.com/ryansolid/solid/blob/master/packages/babel-preset-solid#readme", @@ -14,6 +14,6 @@ "test": "node test.js" }, "dependencies": { - "babel-plugin-jsx-dom-expressions": "~0.24.7" + "babel-plugin-jsx-dom-expressions": "~0.25.0" } } diff --git a/packages/react-solid-state/package-lock.json b/packages/react-solid-state/package-lock.json index fff7308d0..4e49290b4 100644 --- a/packages/react-solid-state/package-lock.json +++ b/packages/react-solid-state/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-solid-state", - "version": "0.23.11", + "version": "0.24.0-beta.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/react-solid-state/package.json b/packages/react-solid-state/package.json index b5b942ab8..f1594eaa8 100644 --- a/packages/react-solid-state/package.json +++ b/packages/react-solid-state/package.json @@ -1,7 +1,7 @@ { "name": "react-solid-state", "description": "Auto tracking state management for modern React", - "version": "0.23.11", + "version": "0.24.0-beta.4", "author": "Ryan Carniato", "license": "MIT", "homepage": "https://github.com/ryansolid/solid/blob/master/packages/react-solid-state#readme", @@ -17,7 +17,7 @@ "test:coverage": "jest --coverage" }, "dependencies": { - "solid-js": "^0.23.11" + "solid-js": "^0.24.0-beta.4" }, "peerDependencies": { "react": "*", diff --git a/packages/solid-element/package.json b/packages/solid-element/package.json index 82cf9ebf8..64a2de167 100644 --- a/packages/solid-element/package.json +++ b/packages/solid-element/package.json @@ -3,7 +3,7 @@ "description": "Webcomponents wrapper for Solid", "author": "Ryan Carniato", "license": "MIT", - "version": "0.23.11", + "version": "0.24.0-beta.4", "homepage": "https://github.com/ryansolid/solid/blob/master/packages/solid-element#readme", "type": "module", "main": "dist/index.js", @@ -23,6 +23,6 @@ "solid-js": "^0.23.0" }, "devDependencies": { - "solid-js": "^0.23.11" + "solid-js": "^0.24.0-beta.4" } } diff --git a/packages/solid-element/src/index.ts b/packages/solid-element/src/index.ts index ae3f8ba59..db8fd2828 100644 --- a/packages/solid-element/src/index.ts +++ b/packages/solid-element/src/index.ts @@ -8,31 +8,39 @@ import { } from "component-register"; export { hot, getCurrentElement } from "component-register"; export type ComponentType = mComponentType; -import { createRoot, createState } from "solid-js"; +import { createRoot, createSignal } from "solid-js"; import { insert } from "solid-js/web"; +function createProps(raw: T) { + const keys = Object.keys(raw) as (keyof T)[]; + const props = {}; + for (let i = 0; i < keys.length; i++) { + const [get, set] = createSignal(raw[keys[i]]); + Object.defineProperty(props, keys[i], { + get, + set + }); + } + return props as T; +} + function withSolid(ComponentType: ComponentType): ComponentType { return (rawProps: T, options: ComponentOptions) => { const { element } = options as { - element: ICustomElement & { _context?: any }; + element: ICustomElement & { _$owner?: any }; }; return createRoot((dispose: Function) => { - const [props, setProps] = createState(rawProps); + const props = createProps(rawProps); - element.addPropertyChangedCallback((key: string, val: any) => - setProps({ [key]: val } as any) - ); + element.addPropertyChangedCallback((key: string, val: any) => (props[key as keyof T] = val)); element.addReleaseCallback(() => { element.renderRoot.textContent = ""; dispose(); }); const comp = (ComponentType as FunctionComponent)(props as T, options); - return insert( - element.renderRoot, - comp - ); - }, (element.assignedSlot && element.assignedSlot._context) || element._context); + return insert(element.renderRoot, comp); + }, (element.assignedSlot && element.assignedSlot._$owner) || element._$owner); }; } diff --git a/packages/solid-meta/babel.config.cjs b/packages/solid-meta/babel.config.cjs index 9ae68fbb6..808f8b52e 100644 --- a/packages/solid-meta/babel.config.cjs +++ b/packages/solid-meta/babel.config.cjs @@ -17,5 +17,19 @@ module.exports = { ] ] } - } + }, + presets: [ + "@babel/preset-typescript" + ], + plugins: [ + [ + "babel-plugin-jsx-dom-expressions", + { + moduleName: "solid-js/web", + contextToCustomElements: true, + wrapConditionals: true, + builtIns: ["For", "Show", "Switch", "Match", "Suspense", "SuspenseList", "Portal"] + } + ] + ] }; diff --git a/packages/solid-meta/package.json b/packages/solid-meta/package.json index 5cf8484e8..cdbd57eb8 100644 --- a/packages/solid-meta/package.json +++ b/packages/solid-meta/package.json @@ -1,7 +1,7 @@ { "name": "solid-meta", "description": "Solid wrapper for Styled JSX", - "version": "0.23.11", + "version": "0.24.0-beta.4", "author": "Ryan Carniato", "license": "MIT", "repository": { @@ -9,15 +9,20 @@ "url": "https://github.com/ryansolid/solid/blob/master/packages/solid-meta" }, "type": "module", - "main": "dist/index.jsx", - "module": "dist/index.jsx", + "main": "dist/index.js", + "exports": { + ".": { + "solid": "./dist/index.jsx", + "default": "./dist/index.js" + } + }, "types": "dist/index.d.ts", "files": [ "dist" ], "sideEffects": false, "scripts": { - "build": "tsc", + "build": "tsc && babel src/index.tsx --out-file dist/index.js", "test": "jest && npm run test:types", "test:types": "tsc --project tsconfig.test.json" }, @@ -25,6 +30,6 @@ "solid-js": "^0.23.0" }, "devDependencies": { - "solid-js": "^0.23.11" + "solid-js": "^0.24.0-beta.4" } } diff --git a/packages/solid-meta/src/index.tsx b/packages/solid-meta/src/index.tsx index ed4db0bc3..a7bd5d62d 100644 --- a/packages/solid-meta/src/index.tsx +++ b/packages/solid-meta/src/index.tsx @@ -8,7 +8,7 @@ import { useContext, Component, JSX, - assignProps + mergeProps } from "solid-js"; import { isServer, Show, Portal, Dynamic } from "solid-js/web"; @@ -136,16 +136,16 @@ export function renderTags(tags: Array) { } export const Title: Component> = props => - MetaTag(assignProps({ tag: "title" }, props)); + MetaTag(mergeProps({ tag: "title" }, props)); export const Style: Component> = props => - MetaTag(assignProps({ tag: "style" }, props)); + MetaTag(mergeProps({ tag: "style" }, props)); export const Meta: Component> = props => - MetaTag(assignProps({ tag: "meta" }, props)); + MetaTag(mergeProps({ tag: "meta" }, props)); export const Link: Component> = props => - MetaTag(assignProps({ tag: "link" }, props)); + MetaTag(mergeProps({ tag: "link" }, props)); export const Base: Component> = props => - MetaTag(assignProps({ tag: "base" }, props)); + MetaTag(mergeProps({ tag: "base" }, props)); diff --git a/packages/solid-rx/package.json b/packages/solid-rx/package.json index dbfa39c15..1785f6513 100644 --- a/packages/solid-rx/package.json +++ b/packages/solid-rx/package.json @@ -1,6 +1,6 @@ { "name": "solid-rx", - "version": "0.23.11", + "version": "0.24.0-beta.4", "description": "Functionally reactive extensions for Solid.js", "author": "Ryan Carniato ", "homepage": "https://github.com/ryansolid/solid/blob/master/packages/solid-rx#readme", @@ -29,6 +29,6 @@ "solid-js": "^0.23.0" }, "devDependencies": { - "solid-js": "^0.23.11" + "solid-js": "^0.24.0-beta.4" } } diff --git a/packages/solid-rx/test/observable.spec.ts b/packages/solid-rx/test/observable.spec.ts index 7824a3922..116812a57 100644 --- a/packages/solid-rx/test/observable.spec.ts +++ b/packages/solid-rx/test/observable.spec.ts @@ -3,15 +3,17 @@ import { observable } from "../src"; describe("Observable operator", () => { test("to observable", async () => { + let out: string; + let set: (string) => void; createRoot(() => { - let out: string; - const [s, set] = createSignal("Hi"), + const [s, _set] = createSignal("Hi"), obsv$ = observable(s); + set = _set; obsv$.subscribe({ next: v => (out = v) }); - expect(out).toBe("Hi"); - set("John"); - expect(out).toBe("John"); }); + expect(out).toBe("Hi"); + set("John"); + expect(out).toBe("John"); }); }); diff --git a/packages/solid-ssr/README.md b/packages/solid-ssr/README.md index cb3991ec8..feef2a49a 100644 --- a/packages/solid-ssr/README.md +++ b/packages/solid-ssr/README.md @@ -2,7 +2,7 @@ This library provides tools to help with SSR. So far it's a simple Static Generator. This project is still in progress. -Look at the examples to best understand how to use it. Important I make use of conditional export maps here in node. You need the latest version of Node 14 (works in latest Node 12 too but this example doesn't, sorry). +Look at the examples to best understand how to use it. Important I make use of conditional export maps here in node. You need the latest version of Node 14 (or latest Node 12 but this example doesn't work in Node 12, sorry). ### Examples diff --git a/packages/solid-ssr/examples/async/index.js b/packages/solid-ssr/examples/async/index.js index bf014d4c4..d257b2087 100644 --- a/packages/solid-ssr/examples/async/index.js +++ b/packages/solid-ssr/examples/async/index.js @@ -1,8 +1,7 @@ import express from "express"; import path from "path"; -import { awaitSuspense } from "solid-js"; -import { renderToString, generateHydrationScript } from "solid-js/web"; +import { renderToStringAsync } from "solid-js/web"; import { extractCss } from "solid-styled-components"; import App from "../shared/src/components/App"; @@ -13,29 +12,26 @@ const lang = "en"; app.use(express.static(path.join(__dirname, "../public"))); app.get("*", async (req, res) => { - let html; + let result; try { - const string = await renderToString(awaitSuspense(() => )); + const { html, script } = await renderToStringAsync(() => ); const style = extractCss(); - html = ` + result = ` 🔥 Solid SSR 🔥 - + ${script} ${style ? `` : ""} -
${string}
+
${html}
`; } catch (err) { console.error(err); } finally { - res.send(html); + res.send(result); } }); diff --git a/packages/solid-ssr/examples/async/rollup.config.js b/packages/solid-ssr/examples/async/rollup.config.js index 128daa207..1e45ab8d1 100644 --- a/packages/solid-ssr/examples/async/rollup.config.js +++ b/packages/solid-ssr/examples/async/rollup.config.js @@ -15,10 +15,10 @@ export default [ preserveEntrySignatures: false, external: ["solid-js", "solid-js/web", "path", "express"], plugins: [ - nodeResolve({ preferBuiltins: true }), + nodeResolve({ preferBuiltins: true, exportConditions: ["solid", "node"] }), babel({ babelHelpers: "bundled", - presets: [["solid", { generate: "ssr", hydratable: true, async: true }]] + presets: [["solid", { generate: "ssr", hydratable: true }]] }), common() ] @@ -33,7 +33,7 @@ export default [ ], preserveEntrySignatures: false, plugins: [ - nodeResolve(), + nodeResolve({ exportConditions: ["solid"] }), babel({ babelHelpers: "bundled", presets: [["solid", { generate: "dom", hydratable: true }]] diff --git a/packages/solid-ssr/examples/shared/src/components/Profile/index.js b/packages/solid-ssr/examples/shared/src/components/Profile/index.js index 3d95e59c0..e0bf97940 100644 --- a/packages/solid-ssr/examples/shared/src/components/Profile/index.js +++ b/packages/solid-ssr/examples/shared/src/components/Profile/index.js @@ -3,25 +3,28 @@ const Profile = lazy(() => import("./Profile")); // this component lazy loads data and code in parallel export default () => { - const [user, loadUser] = createResource(undefined, { name: "profile" }), - [info, loadInfo] = createResource([], { name: "profile_info" }); - loadUser( - () => + const [user] = createResource("user", () => { // simulate data loading - new Promise(res => { + console.log("LOAD USER"); + return new Promise(res => { setTimeout(() => res({ firstName: "Jon", lastName: "Snow" }), 400); - }) - ); - loadInfo( - () => - // simulate cascading data loading - new Promise(res => { - setTimeout( - () => - res(["Something Interesting", "Something else you might care about", "Or maybe not"]), - 800 - ); - }) - ); + }); + }), + [info] = createResource( + () => user() && "userinfo", + () => { + // simulate cascading data loading + console.log("LOAD INFO"); + return new Promise(res => { + setTimeout( + () => + res(["Something Interesting", "Something else you might care about", "Or maybe not"]), + 400 + ); + }); + }, + [] + ); + return ; }; diff --git a/packages/solid-ssr/examples/shared/src/index.js b/packages/solid-ssr/examples/shared/src/index.js index 949ce219e..602838bd9 100644 --- a/packages/solid-ssr/examples/shared/src/index.js +++ b/packages/solid-ssr/examples/shared/src/index.js @@ -2,4 +2,4 @@ import { hydrate } from "solid-js/web"; import App from "./components/App"; // entry point for browser -hydrate(App, document.getElementById("app")); \ No newline at end of file +hydrate(() => , document.getElementById("app")); \ No newline at end of file diff --git a/packages/solid-ssr/examples/ssg/index.js b/packages/solid-ssr/examples/ssg/index.js index 38f10765a..c295cdfa0 100644 --- a/packages/solid-ssr/examples/ssg/index.js +++ b/packages/solid-ssr/examples/ssg/index.js @@ -1,12 +1,11 @@ -import { awaitSuspense } from "solid-js"; -import { renderToString, generateHydrationScript } from "solid-js/web"; +import { renderToStringAsync } from "solid-js/web"; import { extractCss } from "solid-styled-components"; import App from "../shared/src/components/App"; const lang = "en"; // entry point for server render export default async req => { - const string = await renderToString(awaitSuspense(() => )); + const { html, script } = await renderToStringAsync(() => ); const style = extractCss(); return ` @@ -14,10 +13,10 @@ export default async req => { - + ${script} ${style ? `` : ""} -
${string}
+
${html}
`; }; diff --git a/packages/solid-ssr/examples/ssg/rollup.config.js b/packages/solid-ssr/examples/ssg/rollup.config.js index acfba00d4..49d2dcace 100644 --- a/packages/solid-ssr/examples/ssg/rollup.config.js +++ b/packages/solid-ssr/examples/ssg/rollup.config.js @@ -15,10 +15,10 @@ export default [ ], external: ["solid-js", "solid-js/web"], plugins: [ - nodeResolve({ preferBuiltins: true }), + nodeResolve({ preferBuiltins: true, exportConditions: ["solid", "node"] }), babel({ babelHelpers: "bundled", - presets: [["solid", { generate: "ssr", hydratable: true, async: true }]] + presets: [["solid", { generate: "ssr", hydratable: true }]] }), common() ] @@ -33,7 +33,7 @@ export default [ ], preserveEntrySignatures: false, plugins: [ - nodeResolve(), + nodeResolve({ exportConditions: ["solid"] }), babel({ babelHelpers: "bundled", presets: [["solid", { generate: "dom", hydratable: true }]] diff --git a/packages/solid-ssr/examples/ssr/index.js b/packages/solid-ssr/examples/ssr/index.js index 0a5621ff0..ce08ac751 100644 --- a/packages/solid-ssr/examples/ssr/index.js +++ b/packages/solid-ssr/examples/ssr/index.js @@ -1,7 +1,7 @@ import express from "express"; import path from "path"; -import { renderToString, generateHydrationScript } from "solid-js/web"; +import { renderToString } from "solid-js/web"; import App from "../shared/src/components/App"; const app = express(); @@ -11,24 +11,24 @@ const lang = "en"; app.use(express.static(path.join(__dirname, "../public"))); app.get("*", (req, res) => { - let html; + let result; try { - const string = renderToString(() => ); - html = ` + const { html, script } = renderToString(() => ); + result = ` 🔥 Solid SSR 🔥 - + ${script} -
${string}
- +
${html}
+ `; } catch (err) { console.error(err); } finally { - res.send(html); + res.send(result); } }); diff --git a/packages/solid-ssr/examples/ssr/rollup.config.js b/packages/solid-ssr/examples/ssr/rollup.config.js index f72591207..887e3dc15 100644 --- a/packages/solid-ssr/examples/ssr/rollup.config.js +++ b/packages/solid-ssr/examples/ssr/rollup.config.js @@ -14,7 +14,7 @@ export default [ ], external: ["solid-js", "solid-js/web", "path", "express", "stream"], plugins: [ - nodeResolve({ preferBuiltins: true }), + nodeResolve({ preferBuiltins: true, exportConditions: ["solid", "node"] }), babel({ babelHelpers: "bundled", presets: [["solid", { generate: "ssr", hydratable: true }]] @@ -33,7 +33,7 @@ export default [ ], preserveEntrySignatures: false, plugins: [ - nodeResolve(), + nodeResolve({ exportConditions: ["solid"] }), babel({ babelHelpers: "bundled", presets: [["solid", { generate: "dom", hydratable: true }]] diff --git a/packages/solid-ssr/examples/stream/index.js b/packages/solid-ssr/examples/stream/index.js index 2d6661093..06c9f3a76 100644 --- a/packages/solid-ssr/examples/stream/index.js +++ b/packages/solid-ssr/examples/stream/index.js @@ -1,7 +1,7 @@ import express from "express"; import path from "path"; -import { renderToNodeStream, generateHydrationScript } from "solid-js/web"; +import { renderToNodeStream } from "solid-js/web"; import App from "../shared/src/components/App"; const app = express(); @@ -11,7 +11,7 @@ const lang = "en"; app.use(express.static(path.join(__dirname, "../public"))); app.get("*", (req, res) => { - const stream = renderToNodeStream(() => ); + const { stream, script } = renderToNodeStream(() => ); const htmlStart = ` @@ -19,10 +19,8 @@ app.get("*", (req, res) => { - + ${script}
`; diff --git a/packages/solid-ssr/examples/stream/rollup.config.js b/packages/solid-ssr/examples/stream/rollup.config.js index df126b9a7..fed3900b7 100644 --- a/packages/solid-ssr/examples/stream/rollup.config.js +++ b/packages/solid-ssr/examples/stream/rollup.config.js @@ -15,7 +15,7 @@ export default [ ], external: ["solid-js", "solid-js/web", "path", "express", "stream"], plugins: [ - nodeResolve({ preferBuiltins: true }), + nodeResolve({ preferBuiltins: true, exportConditions: ["solid", "node"] }), babel({ babelHelpers: "bundled", presets: [["solid", { generate: "ssr", hydratable: true }]] @@ -33,7 +33,7 @@ export default [ ], preserveEntrySignatures: false, plugins: [ - nodeResolve(), + nodeResolve({ exportConditions: ["solid"] }), babel({ babelHelpers: "bundled", presets: [["solid", { generate: "dom", hydratable: true }]] diff --git a/packages/solid-ssr/package.json b/packages/solid-ssr/package.json index 12609d16d..157561158 100644 --- a/packages/solid-ssr/package.json +++ b/packages/solid-ssr/package.json @@ -1,7 +1,7 @@ { "name": "solid-ssr", "description": "Patches node to work with Solid's SSR", - "version": "0.23.11", + "version": "0.24.0-beta.4", "author": "Ryan Carniato", "license": "MIT", "repository": { @@ -16,18 +16,18 @@ "scripts": { "build:example:async": "rollup -c examples/async/rollup.config.js", "start:example:async": "node examples/async/lib/index.js", - "build:example:ssg": "rollup -c examples/ssg/rollup.config.js && node examples/ssg/export.js", + "build:example:ssg": "rollup -c examples/ssg/rollup.config.js && node --trace-warnings examples/ssg/export.js", "start:example:ssg": "npx serve examples/ssg/public -l 8080", "build:example:ssr": "rollup -c examples/ssr/rollup.config.js", - "start:example:ssr": "node --conditions=server examples/ssr/lib/index.js", + "start:example:ssr": "node examples/ssr/lib/index.js", "build:example:stream": "rollup -c examples/stream/rollup.config.js", - "start:example:stream": "node --conditions=server examples/stream/lib/index.js" + "start:example:stream": "node examples/stream/lib/index.js" }, "devDependencies": { - "babel-preset-solid": "^0.23.8", + "babel-preset-solid": "^0.24.0-beta.3", "express": "^4.17.1", - "solid-js": "^0.23.11", - "solid-styled-components": "^0.23.11" + "solid-js": "^0.24.0-beta.4", + "solid-styled-components": "^0.24.0-beta.4" }, "peerDependencies": { "solid-js": "*" diff --git a/packages/solid-ssr/static/index.js b/packages/solid-ssr/static/index.js index a9844f047..289b35ab1 100644 --- a/packages/solid-ssr/static/index.js +++ b/packages/solid-ssr/static/index.js @@ -4,7 +4,7 @@ const execFile = require("util").promisify(require("child_process").execFile); const pathToRunner = path.resolve(__dirname, "writeToDisk.js"); async function run({ entry, output, url }) { - const { stdout, stderr } = await execFile("node", [pathToRunner, entry, output, url]); + const { stdout, stderr } = await execFile("node", [pathToRunner, entry, output, url, "--trace-warnings"]); if (stdout.length) console.log(stdout); if (stderr.length) console.log(stderr); } diff --git a/packages/solid-styled-components/package.json b/packages/solid-styled-components/package.json index fa385b429..cc532e5dd 100644 --- a/packages/solid-styled-components/package.json +++ b/packages/solid-styled-components/package.json @@ -1,7 +1,7 @@ { "name": "solid-styled-components", "description": "Styled Components for Solid", - "version": "0.23.11", + "version": "0.24.0-beta.4", "author": "Ryan Carniato", "license": "MIT", "repository": { @@ -28,6 +28,6 @@ "solid-js": "^0.23.0" }, "devDependencies": { - "solid-js": "^0.23.11" + "solid-js": "^0.24.0-beta.4" } } diff --git a/packages/solid-styled-components/src/index.js b/packages/solid-styled-components/src/index.js index d7e583cfb..a24344e26 100644 --- a/packages/solid-styled-components/src/index.js +++ b/packages/solid-styled-components/src/index.js @@ -1,5 +1,5 @@ import { css, setup as gooberSetup } from "goober"; -import { assignProps, splitProps, createContext, useContext, createComponent } from "solid-js"; +import { mergeProps, splitProps, createContext, useContext, createComponent } from "solid-js"; import { spread, ssr, ssrSpread, isServer } from "solid-js/web"; export { css, glob, extractCss, keyframes } from "goober"; export function setup(prefixer) { @@ -21,16 +21,16 @@ export function styled(tag) { return (...args) => { return props => { const theme = useContext(ThemeContext); - const clone = assignProps({}, props, { + const clone = mergeProps(props, { get className() { const pClassName = props.className, append = "className" in props && /^go[0-9]+/.test(pClassName); // Call `css` with the append flag and pass the props let className = css.apply({ target: this.target, o: append, p: clone }, args); return [pClassName, className].filter(Boolean).join(" "); - } + }, + theme }); - theme && (clone.theme = theme); const [local, newProps] = splitProps(clone, ["as"]); const createTag = local.as || tag; let el; diff --git a/packages/solid-styled-jsx/package.json b/packages/solid-styled-jsx/package.json index 4b91c7b51..1f2e5e67c 100644 --- a/packages/solid-styled-jsx/package.json +++ b/packages/solid-styled-jsx/package.json @@ -1,7 +1,7 @@ { "name": "solid-styled-jsx", "description": "Solid wrapper for Styled JSX", - "version": "0.23.11", + "version": "0.24.0-beta.4", "author": "Ryan Carniato", "license": "MIT", "repository": { @@ -23,6 +23,6 @@ "styled-jsx": "^3.2.5" }, "devDependencies": { - "solid-js": "^0.23.11" + "solid-js": "^0.24.0-beta.4" } } diff --git a/packages/solid/bench/bench.js b/packages/solid/bench/bench.cjs similarity index 100% rename from packages/solid/bench/bench.js rename to packages/solid/bench/bench.cjs diff --git a/packages/solid/bench/s-mod.js b/packages/solid/bench/s-mod.js index 32b94c44d..e908f94d1 100644 --- a/packages/solid/bench/s-mod.js +++ b/packages/solid/bench/s-mod.js @@ -1,608 +1,601 @@ -// Modified version of S.js[https://github.com/adamhaile/S] by Adam Haile -// Comparator memos from VSJolund fork https://github.com/VSjolund/vs-bind -const equalFn = (a, b) => a === b; -const ERROR = Symbol("error"); -// Public interface -function createRoot(fn, detachedOwner) { - detachedOwner && (Owner = detachedOwner); - let owner = Owner, - listener = Listener, - root = fn.length === 0 ? UNOWNED : createComputationNode(null, null), - result = undefined, - disposer = function _dispose() { - if (RunningClock !== null) { - RootClock.disposes.add(root); - } else { - dispose(root); - } - }; - Owner = root; - Listener = null; - try { - result = fn(disposer); - } catch (err) { - const fns = lookup(Owner, ERROR); - if (!fns) throw err; - fns.forEach(f => f(err)); - } finally { - RootClock.afters.run(f => f()); - Listener = listener; - Owner = owner; - } - return result; -} -function createSignal(value, areEqual) { - const d = new DataNode(value); - let setter; - if (areEqual) { - let age = -1; - setter = v => { - if (!areEqual(v, value)) { - const time = RootClock.time; - if (time === age) { - throw new Error(`Conflicting value update: ${v} is not the same as ${value}`); - } - age = time; - value = v; - d.next(v); - } - }; - } else setter = d.next.bind(d); - return [d.current.bind(d), setter]; -} -function createEffect(fn, value) { - createComputationNode(fn, value); -} -function createDependentEffect(fn, deps, defer) { - const resolved = Array.isArray(deps) ? callAll(deps) : deps; - defer = !!defer; - createComputationNode(value => { - const listener = Listener; - resolved(); - if (defer) defer = false; - else { - Listener = null; - value = fn(value); - Listener = listener; - } - return value; - }); -} -function createMemo(fn, value, areEqual) { - var node = createComputationNode(fn, value); - node.comparator = areEqual || null; - return () => { - if (Listener !== null) { - const state = node.state; - if ((state & 7) !== 0) { - liftComputation(node); - } - if (node.age === RootClock.time && state === 8) { - throw new Error("Circular dependency."); - } - if ((state & 16) === 0) { - if (node.log === null) node.log = createLog(); - logRead(node.log); - } - } - return node.value; - }; -} -function batch(fn) { - let result = undefined; - if (RunningClock !== null) result = fn(); - else { - RunningClock = RootClock; - RunningClock.changes.reset(); - try { - result = fn(); - event(); - } finally { - RunningClock = null; - } - } - return result; -} -function sample(fn) { - let result, - listener = Listener; - Listener = null; - result = fn(); - Listener = listener; - return result; -} -function afterEffects(fn) { - if (RunningClock !== null) RunningClock.afters.add(fn); - else RootClock.afters.add(fn); -} -function onCleanup(fn) { - if (Owner === null) - console.warn("cleanups created outside a `createRoot` or `render` will never be run"); - else if (Owner.cleanups === null) Owner.cleanups = [fn]; - else Owner.cleanups.push(fn); -} -function onError(fn) { - if (Owner === null) - console.warn("error handlers created outside a `createRoot` or `render` will never be run"); - else if (Owner.context === null) Owner.context = { [ERROR]: [fn] }; - else if (!Owner.context[ERROR]) Owner.context[ERROR] = [fn]; - else Owner.context[ERROR].push(fn); -} -function isListening() { - return Listener !== null; -} -function createContext(defaultValue) { - const id = Symbol("context"); - return { id, Provider: createProvider(id), defaultValue }; -} -function useContext(context) { - return lookup(Owner, context.id) || context.defaultValue; -} -function getContextOwner() { - return Owner; -} -function runWithOwner(owner, callback) { - const currentOwner = getContextOwner(); - Owner = owner; - const result = callback(); - Owner = currentOwner; - return result; -} -// Internal implementation -/// Graph classes and operations -class DataNode { - constructor(value) { - this.value = value; - this.pending = NOTPENDING; - this.log = null; - } - current() { - if (Listener !== null) { - if (this.log === null) this.log = createLog(); - logRead(this.log); - } - return this.value; - } - next(value) { - if (RunningClock !== null) { - if (this.pending !== NOTPENDING) { - // value has already been set once, check for conflicts - if (value !== this.pending) { - throw new Error("conflicting changes: " + value + " !== " + this.pending); - } - } else { - // add to list of changes - this.pending = value; - RootClock.changes.add(this); - } - } else { - // not batching, respond to change now - if (this.log !== null) { - this.pending = value; - RootClock.changes.add(this); - event(); - } else { - this.value = value; - } - } - return value; - } -} -function createComputationNode(fn, value) { - const node = { - fn, - value, - age: RootClock.time, - state: 0, - comparator: null, - source1: null, - source1slot: 0, - sources: null, - sourceslots: null, - dependents: null, - dependentslot: 0, - dependentcount: 0, - owner: Owner, - owned: null, - log: null, - context: null, - cleanups: null - }; - if (fn === null) return node; - let owner = Owner, - listener = Listener; - if (owner === null) - console.warn("computations created outside a `createRoot` or `render` will never be disposed"); - Owner = Listener = node; - if (RunningClock === null) { - toplevelComputation(node); - } else node.value = node.fn(node.value); - if (owner && owner !== UNOWNED) { - if (owner.owned === null) owner.owned = [node]; - else owner.owned.push(node); - } - Owner = owner; - Listener = listener; - return node; -} -function createClock() { - return { - time: 0, - changes: new Queue(), - updates: new Queue(), - disposes: new Queue(), - afters: new Queue() - }; -} -function createLog() { - return { - node1: null, - node1slot: 0, - nodes: null, - nodeslots: null - }; -} -class Queue { - constructor() { - this.items = []; - this.count = 0; - } - reset() { - this.count = 0; - } - add(item) { - this.items[this.count++] = item; - } - run(fn) { - let items = this.items; - for (let i = 0; i < this.count; i++) { - try { - const item = items[i]; - items[i] = null; - fn(item); - } catch (err) { - const fns = lookup(Owner, ERROR); - if (!fns) throw err; - fns.forEach(f => f(err)); - } - } - this.count = 0; - } -} -// "Globals" used to keep track of current system state -let RootClock = createClock(), - RunningClock = null, // currently running clock - Listener = null, // currently listening computation - Owner = null, // owner for new computations - Pending = null; // pending node -// Constants -let NOTPENDING = {}, - UNOWNED = createComputationNode(null, null); -// State -// 1 - Stale, 2 - Pending, 4 - Pending Disposal, 8 - Running, 16 - Disposed -// Functions -function callAll(ss) { - return function all() { - for (let i = 0; i < ss.length; i++) ss[i](); - }; -} -function lookup(owner, key) { - return ( - owner && ((owner.context && owner.context[key]) || (owner.owner && lookup(owner.owner, key))) - ); -} -function resolveChildren(children) { - if (typeof children === "function") return createMemo(() => resolveChildren(children())); - if (Array.isArray(children)) { - const results = []; - for (let i = 0; i < children.length; i++) { - let result = resolveChildren(children[i]); - Array.isArray(result) ? results.push.apply(results, result) : results.push(result); - } - return results; - } - return children; -} -function createProvider(id) { - return function provider(props) { - let rendered; - createComputationNode(() => { - Owner.context = { [id]: props.value }; - rendered = sample(() => resolveChildren(props.children)); - }); - return rendered; - }; -} -function logRead(from) { - let to = Listener, - fromslot, - toslot = to.source1 === null ? -1 : to.sources === null ? 0 : to.sources.length; - if (from.node1 === null) { - from.node1 = to; - from.node1slot = toslot; - fromslot = -1; - } else if (from.nodes === null) { - if (from.node1 === to) return; - from.nodes = [to]; - from.nodeslots = [toslot]; - fromslot = 0; - } else { - fromslot = from.nodes.length; - if (from.nodes[fromslot - 1] === to) return; - from.nodes.push(to); - from.nodeslots.push(toslot); - } - if (to.source1 === null) { - to.source1 = from; - to.source1slot = fromslot; - } else if (to.sources === null) { - to.sources = [from]; - to.sourceslots = [fromslot]; - } else { - to.sources.push(from); - to.sourceslots.push(fromslot); - } -} -function liftComputation(node) { - if ((node.state & 6) !== 0) { - applyUpstreamUpdates(node); - } - if ((node.state & 1) !== 0) { - updateNode(node); - } - resetComputation(node, 31); -} -function event() { - // b/c we might be under a top level S.root(), have to preserve current root - let owner = Owner; - RootClock.updates.reset(); - RootClock.time++; - try { - run(RootClock); - } finally { - RunningClock = Listener = null; - Owner = owner; - } -} -function toplevelComputation(node) { - RunningClock = RootClock; - RootClock.changes.reset(); - RootClock.updates.reset(); - try { - node.value = node.fn(node.value); - if (RootClock.changes.count > 0 || RootClock.updates.count > 0) { - RootClock.time++; - run(RootClock); - } - } catch (err) { - const fns = lookup(Owner, ERROR); - if (!fns) throw err; - fns.forEach(f => f(err)); - } finally { - RunningClock = Owner = Listener = null; - } -} -function run(clock) { - let running = RunningClock, - count = 0; - RunningClock = clock; - clock.disposes.reset(); - // for each batch ... - while (clock.changes.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) { - if (count > 0) - // don't tick on first run, or else we expire already scheduled updates - clock.time++; - clock.changes.run(applyDataChange); - clock.updates.run(updateNode); - clock.disposes.run(dispose); - // if there are still changes after excessive batches, assume runaway - if (count++ > 1e5) { - throw new Error("Runaway clock detected"); - } - } - clock.afters.run(f => f()); - RunningClock = running; -} -function applyDataChange(data) { - data.value = data.pending; - data.pending = NOTPENDING; - if (data.log) setComputationState(data.log, stateStale); -} -function updateNode(node) { - const state = node.state; - if ((state & 16) === 0) { - if ((state & 2) !== 0) { - node.dependents[node.dependentslot++] = null; - if (node.dependentslot === node.dependentcount) { - resetComputation(node, 14); - } - } else if ((state & 1) !== 0) { - if ((state & 4) !== 0) { - liftComputation(node); - } else if (node.comparator) { - const current = updateComputation(node); - const comparator = node.comparator; - if (!comparator(current, node.value)) { - markDownstreamComputations(node, false, true); - } - } else { - updateComputation(node); - } - } - } -} -function updateComputation(node) { - const value = node.value, - owner = Owner, - listener = Listener; - Owner = Listener = node; - node.state = 8; - cleanupNode(node, false); - node.value = node.fn(node.value); - resetComputation(node, 31); - Owner = owner; - Listener = listener; - return value; -} -function stateStale(node) { - const time = RootClock.time; - if (node.age < time) { - node.state |= 1; - node.age = time; - setDownstreamState(node, !!node.comparator); - } -} -function statePending(node) { - const time = RootClock.time; - if (node.age < time) { - node.state |= 2; - let dependents = node.dependents || (node.dependents = []); - dependents[node.dependentcount++] = Pending; - setDownstreamState(node, true); - } -} -function pendingStateStale(node) { - if ((node.state & 2) !== 0) { - node.state = 1; - const time = RootClock.time; - if (node.age < time) { - node.age = time; - if (!node.comparator) { - markDownstreamComputations(node, false, true); - } - } - } -} -function setDownstreamState(node, pending) { - RootClock.updates.add(node); - if (node.comparator) { - const pending = Pending; - Pending = node; - markDownstreamComputations(node, true, false); - Pending = pending; - } else { - markDownstreamComputations(node, pending, false); - } -} -function markDownstreamComputations(node, onchange, dirty) { - const owned = node.owned; - if (owned !== null) { - const pending = onchange && !dirty; - markForDisposal(owned, pending, RootClock.time); - } - const log = node.log; - if (log !== null) { - setComputationState(log, dirty ? pendingStateStale : onchange ? statePending : stateStale); - } -} -function setComputationState(log, stateFn) { - const node1 = log.node1, - nodes = log.nodes; - if (node1 !== null) stateFn(node1); - if (nodes !== null) { - for (let i = 0, ln = nodes.length; i < ln; i++) { - stateFn(nodes[i]); - } - } -} -function markForDisposal(children, pending, time) { - for (let i = 0, ln = children.length; i < ln; i++) { - const child = children[i]; - if (child !== null) { - if (pending) { - if ((child.state & 16) === 0) { - child.state |= 4; - } - } else { - child.age = time; - child.state = 16; - } - const owned = child.owned; - if (owned !== null) markForDisposal(owned, pending, time); - } - } -} -function applyUpstreamUpdates(node) { - if ((node.state & 4) !== 0) { - const owner = node.owner; - if ((owner.state & 7) !== 0) liftComputation(owner); - node.state &= ~4; - } - if ((node.state & 2) !== 0) { - const slots = node.dependents; - for (let i = node.dependentslot, ln = node.dependentcount; i < ln; i++) { - const slot = slots[i]; - if (slot != null) liftComputation(slot); - slots[i] = null; - } - node.state &= ~2; - } -} -function cleanupNode(node, final) { - let source1 = node.source1, - sources = node.sources, - sourceslots = node.sourceslots, - cleanups = node.cleanups, - owned = node.owned, - i, - len; - if (cleanups !== null) { - for (i = 0; i < cleanups.length; i++) { - cleanups[i](final); - } - node.cleanups = null; - } - node.context = null; - if (owned !== null) { - for (i = 0; i < owned.length; i++) { - dispose(owned[i]); - } - node.owned = null; - } - if (source1 !== null) { - cleanupSource(source1, node.source1slot); - node.source1 = null; - } - if (sources !== null) { - for (i = 0, len = sources.length; i < len; i++) { - cleanupSource(sources.pop(), sourceslots.pop()); - } - } -} -function cleanupSource(source, slot) { - let nodes = source.nodes, - nodeslots = source.nodeslots, - last, - lastslot; - if (slot === -1) { - source.node1 = null; - } else { - last = nodes.pop(); - lastslot = nodeslots.pop(); - if (slot !== nodes.length) { - nodes[slot] = last; - nodeslots[slot] = lastslot; - if (lastslot === -1) { - last.source1slot = slot; - } else { - last.sourceslots[lastslot] = slot; - } - } - } -} -function resetComputation(node, flags) { - node.state &= ~flags; - node.dependentslot = 0; - node.dependentcount = 0; -} -function dispose(node) { - node.fn = null; - node.log = null; - node.dependents = null; - cleanupNode(node, true); - resetComputation(node, 31); -} - -module.exports = { - createRoot, createComputed: createEffect, createSignal -} +// Modified version of S.js[https://github.com/adamhaile/S] by Adam Haile +// Comparator memos from VSJolund fork https://github.com/VSjolund/vs-bind +const equalFn = (a, b) => a === b; +const ERROR = Symbol("error"); +// Public interface +function createRoot(fn, detachedOwner) { + detachedOwner && (Owner = detachedOwner); + let owner = Owner, + listener = Listener, + root = fn.length === 0 ? UNOWNED : createComputationNode(null, null), + result = undefined, + disposer = function _dispose() { + if (RunningClock !== null) { + RootClock.disposes.add(root); + } else { + dispose(root); + } + }; + Owner = root; + Listener = null; + try { + result = fn(disposer); + } catch (err) { + const fns = lookup(Owner, ERROR); + if (!fns) throw err; + fns.forEach(f => f(err)); + } finally { + RootClock.afters.run(f => f()); + Listener = listener; + Owner = owner; + } + return result; +} +function createSignal(value, areEqual) { + const d = new DataNode(value); + let setter; + if (areEqual) { + let age = -1; + setter = v => { + if (!areEqual(v, value)) { + const time = RootClock.time; + if (time === age) { + throw new Error(`Conflicting value update: ${v} is not the same as ${value}`); + } + age = time; + value = v; + d.next(v); + } + }; + } else setter = d.next.bind(d); + return [d.current.bind(d), setter]; +} +function createEffect(fn, value) { + createComputationNode(fn, value); +} +function createDependentEffect(fn, deps, defer) { + const resolved = Array.isArray(deps) ? callAll(deps) : deps; + defer = !!defer; + createComputationNode(value => { + const listener = Listener; + resolved(); + if (defer) defer = false; + else { + Listener = null; + value = fn(value); + Listener = listener; + } + return value; + }); +} +function createMemo(fn, value, areEqual) { + var node = createComputationNode(fn, value); + node.comparator = areEqual || null; + return () => { + if (Listener !== null) { + const state = node.state; + if ((state & 7) !== 0) { + liftComputation(node); + } + if (node.age === RootClock.time && state === 8) { + throw new Error("Circular dependency."); + } + if ((state & 16) === 0) { + if (node.log === null) node.log = createLog(); + logRead(node.log); + } + } + return node.value; + }; +} +function batch(fn) { + let result = undefined; + if (RunningClock !== null) result = fn(); + else { + RunningClock = RootClock; + RunningClock.changes.reset(); + try { + result = fn(); + event(); + } finally { + RunningClock = null; + } + } + return result; +} +function sample(fn) { + let result, + listener = Listener; + Listener = null; + result = fn(); + Listener = listener; + return result; +} +function afterEffects(fn) { + if (RunningClock !== null) RunningClock.afters.add(fn); + else RootClock.afters.add(fn); +} +function onCleanup(fn) { + if (Owner === null) + console.warn("cleanups created outside a `createRoot` or `render` will never be run"); + else if (Owner.cleanups === null) Owner.cleanups = [fn]; + else Owner.cleanups.push(fn); +} +function onError(fn) { + if (Owner === null) + console.warn("error handlers created outside a `createRoot` or `render` will never be run"); + else if (Owner.context === null) Owner.context = { [ERROR]: [fn] }; + else if (!Owner.context[ERROR]) Owner.context[ERROR] = [fn]; + else Owner.context[ERROR].push(fn); +} +function isListening() { + return Listener !== null; +} +function createContext(defaultValue) { + const id = Symbol("context"); + return { id, Provider: createProvider(id), defaultValue }; +} +function useContext(context) { + return lookup(Owner, context.id) || context.defaultValue; +} +function getOwner() { + return Owner; +} +// Internal implementation +/// Graph classes and operations +class DataNode { + constructor(value) { + this.value = value; + this.pending = NOTPENDING; + this.log = null; + } + current() { + if (Listener !== null) { + if (this.log === null) this.log = createLog(); + logRead(this.log); + } + return this.value; + } + next(value) { + if (RunningClock !== null) { + if (this.pending !== NOTPENDING) { + // value has already been set once, check for conflicts + if (value !== this.pending) { + throw new Error("conflicting changes: " + value + " !== " + this.pending); + } + } else { + // add to list of changes + this.pending = value; + RootClock.changes.add(this); + } + } else { + // not batching, respond to change now + if (this.log !== null) { + this.pending = value; + RootClock.changes.add(this); + event(); + } else { + this.value = value; + } + } + return value; + } +} +function createComputationNode(fn, value) { + const node = { + fn, + value, + age: RootClock.time, + state: 0, + comparator: null, + source1: null, + source1slot: 0, + sources: null, + sourceslots: null, + dependents: null, + dependentslot: 0, + dependentcount: 0, + owner: Owner, + owned: null, + log: null, + context: null, + cleanups: null + }; + if (fn === null) return node; + let owner = Owner, + listener = Listener; + if (owner === null) + console.warn("computations created outside a `createRoot` or `render` will never be disposed"); + Owner = Listener = node; + if (RunningClock === null) { + toplevelComputation(node); + } else node.value = node.fn(node.value); + if (owner && owner !== UNOWNED) { + if (owner.owned === null) owner.owned = [node]; + else owner.owned.push(node); + } + Owner = owner; + Listener = listener; + return node; +} +function createClock() { + return { + time: 0, + changes: new Queue(), + updates: new Queue(), + disposes: new Queue(), + afters: new Queue() + }; +} +function createLog() { + return { + node1: null, + node1slot: 0, + nodes: null, + nodeslots: null + }; +} +class Queue { + constructor() { + this.items = []; + this.count = 0; + } + reset() { + this.count = 0; + } + add(item) { + this.items[this.count++] = item; + } + run(fn) { + let items = this.items; + for (let i = 0; i < this.count; i++) { + try { + const item = items[i]; + items[i] = null; + fn(item); + } catch (err) { + const fns = lookup(Owner, ERROR); + if (!fns) throw err; + fns.forEach(f => f(err)); + } + } + this.count = 0; + } +} +// "Globals" used to keep track of current system state +let RootClock = createClock(), + RunningClock = null, // currently running clock + Listener = null, // currently listening computation + Owner = null, // owner for new computations + Pending = null; // pending node +// Constants +let NOTPENDING = {}, + UNOWNED = createComputationNode(null, null); +// State +// 1 - Stale, 2 - Pending, 4 - Pending Disposal, 8 - Running, 16 - Disposed +// Functions +function callAll(ss) { + return function all() { + for (let i = 0; i < ss.length; i++) ss[i](); + }; +} +function lookup(owner, key) { + return ( + owner && ((owner.context && owner.context[key]) || (owner.owner && lookup(owner.owner, key))) + ); +} +function resolveChildren(children) { + if (typeof children === "function") return createMemo(() => resolveChildren(children())); + if (Array.isArray(children)) { + const results = []; + for (let i = 0; i < children.length; i++) { + let result = resolveChildren(children[i]); + Array.isArray(result) ? results.push.apply(results, result) : results.push(result); + } + return results; + } + return children; +} +function createProvider(id) { + return function provider(props) { + let rendered; + createComputationNode(() => { + Owner.context = { [id]: props.value }; + rendered = sample(() => resolveChildren(props.children)); + }); + return rendered; + }; +} +function logRead(from) { + let to = Listener, + fromslot, + toslot = to.source1 === null ? -1 : to.sources === null ? 0 : to.sources.length; + if (from.node1 === null) { + from.node1 = to; + from.node1slot = toslot; + fromslot = -1; + } else if (from.nodes === null) { + if (from.node1 === to) return; + from.nodes = [to]; + from.nodeslots = [toslot]; + fromslot = 0; + } else { + fromslot = from.nodes.length; + if (from.nodes[fromslot - 1] === to) return; + from.nodes.push(to); + from.nodeslots.push(toslot); + } + if (to.source1 === null) { + to.source1 = from; + to.source1slot = fromslot; + } else if (to.sources === null) { + to.sources = [from]; + to.sourceslots = [fromslot]; + } else { + to.sources.push(from); + to.sourceslots.push(fromslot); + } +} +function liftComputation(node) { + if ((node.state & 6) !== 0) { + applyUpstreamUpdates(node); + } + if ((node.state & 1) !== 0) { + updateNode(node); + } + resetComputation(node, 31); +} +function event() { + // b/c we might be under a top level S.root(), have to preserve current root + let owner = Owner; + RootClock.updates.reset(); + RootClock.time++; + try { + run(RootClock); + } finally { + RunningClock = Listener = null; + Owner = owner; + } +} +function toplevelComputation(node) { + RunningClock = RootClock; + RootClock.changes.reset(); + RootClock.updates.reset(); + try { + node.value = node.fn(node.value); + if (RootClock.changes.count > 0 || RootClock.updates.count > 0) { + RootClock.time++; + run(RootClock); + } + } catch (err) { + const fns = lookup(Owner, ERROR); + if (!fns) throw err; + fns.forEach(f => f(err)); + } finally { + RunningClock = Owner = Listener = null; + } +} +function run(clock) { + let running = RunningClock, + count = 0; + RunningClock = clock; + clock.disposes.reset(); + // for each batch ... + while (clock.changes.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) { + if (count > 0) + // don't tick on first run, or else we expire already scheduled updates + clock.time++; + clock.changes.run(applyDataChange); + clock.updates.run(updateNode); + clock.disposes.run(dispose); + // if there are still changes after excessive batches, assume runaway + if (count++ > 1e5) { + throw new Error("Runaway clock detected"); + } + } + clock.afters.run(f => f()); + RunningClock = running; +} +function applyDataChange(data) { + data.value = data.pending; + data.pending = NOTPENDING; + if (data.log) setComputationState(data.log, stateStale); +} +function updateNode(node) { + const state = node.state; + if ((state & 16) === 0) { + if ((state & 2) !== 0) { + node.dependents[node.dependentslot++] = null; + if (node.dependentslot === node.dependentcount) { + resetComputation(node, 14); + } + } else if ((state & 1) !== 0) { + if ((state & 4) !== 0) { + liftComputation(node); + } else if (node.comparator) { + const current = updateComputation(node); + const comparator = node.comparator; + if (!comparator(current, node.value)) { + markDownstreamComputations(node, false, true); + } + } else { + updateComputation(node); + } + } + } +} +function updateComputation(node) { + const value = node.value, + owner = Owner, + listener = Listener; + Owner = Listener = node; + node.state = 8; + cleanupNode(node, false); + node.value = node.fn(node.value); + resetComputation(node, 31); + Owner = owner; + Listener = listener; + return value; +} +function stateStale(node) { + const time = RootClock.time; + if (node.age < time) { + node.state |= 1; + node.age = time; + setDownstreamState(node, !!node.comparator); + } +} +function statePending(node) { + const time = RootClock.time; + if (node.age < time) { + node.state |= 2; + let dependents = node.dependents || (node.dependents = []); + dependents[node.dependentcount++] = Pending; + setDownstreamState(node, true); + } +} +function pendingStateStale(node) { + if ((node.state & 2) !== 0) { + node.state = 1; + const time = RootClock.time; + if (node.age < time) { + node.age = time; + if (!node.comparator) { + markDownstreamComputations(node, false, true); + } + } + } +} +function setDownstreamState(node, pending) { + RootClock.updates.add(node); + if (node.comparator) { + const pending = Pending; + Pending = node; + markDownstreamComputations(node, true, false); + Pending = pending; + } else { + markDownstreamComputations(node, pending, false); + } +} +function markDownstreamComputations(node, onchange, dirty) { + const owned = node.owned; + if (owned !== null) { + const pending = onchange && !dirty; + markForDisposal(owned, pending, RootClock.time); + } + const log = node.log; + if (log !== null) { + setComputationState(log, dirty ? pendingStateStale : onchange ? statePending : stateStale); + } +} +function setComputationState(log, stateFn) { + const node1 = log.node1, + nodes = log.nodes; + if (node1 !== null) stateFn(node1); + if (nodes !== null) { + for (let i = 0, ln = nodes.length; i < ln; i++) { + stateFn(nodes[i]); + } + } +} +function markForDisposal(children, pending, time) { + for (let i = 0, ln = children.length; i < ln; i++) { + const child = children[i]; + if (child !== null) { + if (pending) { + if ((child.state & 16) === 0) { + child.state |= 4; + } + } else { + child.age = time; + child.state = 16; + } + const owned = child.owned; + if (owned !== null) markForDisposal(owned, pending, time); + } + } +} +function applyUpstreamUpdates(node) { + if ((node.state & 4) !== 0) { + const owner = node.owner; + if ((owner.state & 7) !== 0) liftComputation(owner); + node.state &= ~4; + } + if ((node.state & 2) !== 0) { + const slots = node.dependents; + for (let i = node.dependentslot, ln = node.dependentcount; i < ln; i++) { + const slot = slots[i]; + if (slot != null) liftComputation(slot); + slots[i] = null; + } + node.state &= ~2; + } +} +function cleanupNode(node, final) { + let source1 = node.source1, + sources = node.sources, + sourceslots = node.sourceslots, + cleanups = node.cleanups, + owned = node.owned, + i, + len; + if (cleanups !== null) { + for (i = 0; i < cleanups.length; i++) { + cleanups[i](final); + } + node.cleanups = null; + } + node.context = null; + if (owned !== null) { + for (i = 0; i < owned.length; i++) { + dispose(owned[i]); + } + node.owned = null; + } + if (source1 !== null) { + cleanupSource(source1, node.source1slot); + node.source1 = null; + } + if (sources !== null) { + for (i = 0, len = sources.length; i < len; i++) { + cleanupSource(sources.pop(), sourceslots.pop()); + } + } +} +function cleanupSource(source, slot) { + let nodes = source.nodes, + nodeslots = source.nodeslots, + last, + lastslot; + if (slot === -1) { + source.node1 = null; + } else { + last = nodes.pop(); + lastslot = nodeslots.pop(); + if (slot !== nodes.length) { + nodes[slot] = last; + nodeslots[slot] = lastslot; + if (lastslot === -1) { + last.source1slot = slot; + } else { + last.sourceslots[lastslot] = slot; + } + } + } +} +function resetComputation(node, flags) { + node.state &= ~flags; + node.dependentslot = 0; + node.dependentcount = 0; +} +function dispose(node) { + node.fn = null; + node.log = null; + node.dependents = null; + cleanupNode(node, true); + resetComputation(node, 31); +} + +module.exports = { + createRoot, createComputed: createEffect, createSignal +} \ No newline at end of file diff --git a/packages/solid/dom/package.json b/packages/solid/dom/package.json deleted file mode 100644 index 646acd807..000000000 --- a/packages/solid/dom/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "solid-js/dom", - "main": "../web/dist/web.cjs", - "module": "../web/dist/web.js", - "types": "../web/types/index.d.ts", - "type": "module", - "sideEffects": false -} \ No newline at end of file diff --git a/packages/solid/html/src/index.ts b/packages/solid/html/src/index.ts index 24612bb32..734454bbe 100644 --- a/packages/solid/html/src/index.ts +++ b/packages/solid/html/src/index.ts @@ -12,7 +12,7 @@ import { Aliases, Properties, ChildProperties, - NonComposedEvents, + DelegatedEvents, SVGElements, SVGNamespace } from "solid-js/web"; @@ -30,7 +30,7 @@ export default createHTML({ Aliases, Properties, ChildProperties, - NonComposedEvents, + DelegatedEvents, SVGElements, SVGNamespace }); diff --git a/packages/solid/package.json b/packages/solid/package.json index 5bbb3b379..59ede1252 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,7 +1,7 @@ { "name": "solid-js", "description": "A declarative JavaScript library for building user interfaces.", - "version": "0.23.11", + "version": "0.24.0-beta.4", "author": "Ryan Carniato", "license": "MIT", "homepage": "https://github.com/ryansolid/solid#readme", @@ -29,7 +29,11 @@ ], "exports": { ".": { - "server": { + "browser": { + "import": "./dist/solid.js", + "require": "./dist/solid.cjs" + }, + "node": { "import": "./dist/static.js", "require": "./dist/static.cjs" }, @@ -37,21 +41,13 @@ "require": "./dist/solid.cjs" }, "./web": { - "server-async": { - "import": "./web/dist/server-async.js", - "require": "./web/dist/server-async.cjs" - }, - "server": { - "import": "./web/dist/server.js", - "require": "./web/dist/server.cjs" - }, "browser": { "import": "./web/dist/web.js", "require": "./web/dist/web.cjs" }, "node": { - "import": "./web/dist/server-async.js", - "require": "./web/dist/server-async.cjs" + "import": "./web/dist/server.js", + "require": "./web/dist/server.cjs" }, "import": "./web/dist/web.js", "require": "./web/dist/web.cjs" @@ -67,10 +63,6 @@ "./dev": { "import": "./dev/dist/dev.js", "require": "./dev/dist/dev.cjs" - }, - "./dom": { - "import": "./web/dist/web.js", - "require": "./web/dist/web.cjs" } }, "scripts": { @@ -82,7 +74,7 @@ "build:types-web": "tsc --project ./web/tsconfig.json && tsconfig-replace-paths --project ./web/tsconfig.types.json", "build:types-html": "tsc --project ./html/tsconfig.json", "build:types-h": "tsc --project ./h/tsconfig.json", - "bench": "node --allow-natives-syntax bench/bench.js", + "bench": "node --allow-natives-syntax bench/bench.cjs", "test": "jest && npm run test:types", "test:coverage": "jest --coverage && npm run test:types", "test:types": "tsc --project tsconfig.test.json", diff --git a/packages/solid/rollup.config.js b/packages/solid/rollup.config.js index 83fc6c4ac..a98c9e6c6 100644 --- a/packages/solid/rollup.config.js +++ b/packages/solid/rollup.config.js @@ -103,10 +103,10 @@ export default [ copy({ targets: [ { - src: ["../../node_modules/dom-expressions/src/runtime.d.ts"], + src: ["../../node_modules/dom-expressions/src/client.d.ts"], dest: "./web/src/" }, - { src: "../../node_modules/dom-expressions/src/runtime.d.ts", dest: "./web/types/" } + { src: "../../node_modules/dom-expressions/src/client.d.ts", dest: "./web/types/" } ] }) ].concat(plugins) @@ -160,35 +160,11 @@ export default [ copy({ targets: [ { - src: ["../../node_modules/dom-expressions/src/syncSSR.d.ts"], + src: ["../../node_modules/dom-expressions/src/server.d.ts"], dest: "./web/server" } ] }) ].concat(plugins) - }, - { - input: "web/server-async/index.ts", - output: [ - { - file: "web/dist/server-async.cjs", - format: "cjs" - }, - { - file: "web/dist/server-async.js", - format: "es" - } - ], - external: ["solid-js"], - plugins: [ - copy({ - targets: [ - { - src: ["../../node_modules/dom-expressions/src/asyncSSR.d.ts"], - dest: "./web/server-async" - } - ] - }) - ].concat(plugins) } ]; diff --git a/packages/solid/src/index.ts b/packages/solid/src/index.ts index d0904c9a6..4121c2b48 100644 --- a/packages/solid/src/index.ts +++ b/packages/solid/src/index.ts @@ -1,51 +1,51 @@ -export { - createRoot, - createSignal, - createEffect, - createRenderEffect, - createComputed, - createDeferred, - createSelector, - createMemo, - createResource, - getListener, - onMount, - onCleanup, - onError, - untrack, - batch, - on, - useTransition, - createContext, - useContext, - getContextOwner, - runWithOwner, - equalFn, - serializeGraph -} from "./reactive/signal"; -export type { Resource } from "./reactive/signal"; - -export { createState, unwrap, $RAW } from "./reactive/state"; -export type { State, SetStateFunction } from "./reactive/state"; -export * from "./reactive/resourceState"; -export * from "./reactive/mutable"; - -export { reconcile, produce } from "./reactive/stateModifiers"; - -export * from "./reactive/scheduler"; -export * from "./reactive/array"; -export * from "./render"; -export type { JSX } from "./jsx"; - -// handle multiple instance check -declare global { - var Solid$$: boolean; -} - -if ("_SOLID_DEV_" && globalThis) { - if (!globalThis.Solid$$) globalThis.Solid$$ = true; - else - console.warn( - "You appear to have multiple instances of Solid. This can lead to unexpected behavior." - ); -} +export { + createRoot, + createSignal, + createEffect, + createRenderEffect, + createComputed, + createDeferred, + createSelector, + createMemo, + createResource, + getListener, + onMount, + onCleanup, + onError, + untrack, + batch, + on, + useTransition, + createContext, + useContext, + children, + getOwner, + runWithOwner, + equalFn, + serializeGraph +} from "./reactive/signal"; +export type { Resource } from "./reactive/signal"; + +export { createState, unwrap, $RAW } from "./reactive/state"; +export type { State, SetStateFunction } from "./reactive/state"; +export * from "./reactive/mutable"; + +export { reconcile, produce } from "./reactive/stateModifiers"; + +export * from "./reactive/scheduler"; +export * from "./reactive/array"; +export * from "./render"; +export type { JSX } from "./jsx"; + +// handle multiple instance check +declare global { + var Solid$$: boolean; +} + +if ("_SOLID_DEV_" && globalThis) { + if (!globalThis.Solid$$) globalThis.Solid$$ = true; + else + console.warn( + "You appear to have multiple instances of Solid. This can lead to unexpected behavior." + ); +} diff --git a/packages/solid/src/reactive/mutable.ts b/packages/solid/src/reactive/mutable.ts index 655c90735..05ad3388b 100644 --- a/packages/solid/src/reactive/mutable.ts +++ b/packages/solid/src/reactive/mutable.ts @@ -1,6 +1,5 @@ -import { Listener, createSignal, hashValue, registerGraph } from "./signal"; +import { Listener, createSignal, hashValue, registerGraph, batch } from "./signal"; import { - wrap, unwrap, isWrappable, getDataNodes, @@ -41,12 +40,7 @@ const proxyTraps: ProxyHandler = { node[0](); } return wrappable - ? wrap( - value, - "_SOLID_DEV_" && target[$NAME] && `${target[$NAME]}:${property as string}`, - false, - proxyTraps - ) + ? wrap(value, "_SOLID_DEV_" && target[$NAME] && `${target[$NAME]}:${property as string}`) : value; }, @@ -63,19 +57,38 @@ const proxyTraps: ProxyHandler = { getOwnPropertyDescriptor: proxyDescriptor }; +function wrap(value: T, name?: string): State { + let p = value[$PROXY]; + if (!p) { + Object.defineProperty(value, $PROXY, { value: (p = new Proxy(value, proxyTraps)) }); + let keys = Object.keys(value), + desc = Object.getOwnPropertyDescriptors(value); + for (let i = 0, l = keys.length; i < l; i++) { + const prop = keys[i]; + if (desc[prop].set) { + const og = desc[prop].set!, + set = (v: T[keyof T]) => batch(() => og.call(p, v)); + Object.defineProperty(value, prop, { + set + }); + } + } + if ("_SOLID_DEV_" && name) Object.defineProperty(value, $NAME, { value: name }); + } + return p; +} + export function createMutable( state: T | State, options?: { name?: string } ): State { - const unwrappedState = unwrap(state || {}, true); + const unwrappedState = unwrap(state || {}); const wrappedState = wrap( unwrappedState, - "_SOLID_DEV_" && ((options && options.name) || hashValue(unwrappedState)), - true, - proxyTraps + "_SOLID_DEV_" && ((options && options.name) || hashValue(unwrappedState)) ); if ("_SOLID_DEV_") { - const name = options && options.name || hashValue(unwrappedState); + const name = (options && options.name) || hashValue(unwrappedState); registerGraph(name, { value: unwrappedState }); } return wrappedState as State; diff --git a/packages/solid/src/reactive/resourceState.ts b/packages/solid/src/reactive/resourceState.ts deleted file mode 100644 index 28d5635c2..000000000 --- a/packages/solid/src/reactive/resourceState.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { batch, createSignal, Listener, createResource, hashValue, registerGraph } from "./signal"; - -import { - updatePath, - wrap, - unwrap, - isWrappable, - getDataNodes, - $RAW, - $NODE, - $PROXY, - StateNode, - SetStateFunction, - State, - setProperty, - proxyDescriptor -} from "./state"; - -function createResourceNode(v: any, name?: string) { - const [r, load] = createResource(v, { name }); - return [() => r(), (v: any) => load(() => v), load, () => r.loading]; -} - -export interface LoadStateFunction { - ( - v: { [P in keyof T]?: () => Promise | T[P] }, - reconcilerFn?: (v: Partial) => (state: State) => void - ): void; -} - -export function createResourceState( - state: T | State, - options: { name?: string } = {} -): [ - State, - LoadStateFunction, - SetStateFunction -] { - const loadingTraps: ProxyHandler = { - get(nodes, property: string | number) { - const node = - nodes[property] || - (nodes[property] = createResourceNode( - undefined, - options.name && `${options.name}:${property}` - )); - return node[3](); - }, - - set() { - return true; - }, - - deleteProperty() { - return true; - } - }; - - const resourceTraps: ProxyHandler = { - get(target, property, receiver) { - if (property === $RAW) return target; - if (property === $PROXY) return receiver; - if (property === "loading") return new Proxy(getDataNodes(target), loadingTraps); - const value = target[property as string | number]; - if (property === $NODE || property === "__proto__") return value; - - const wrappable = isWrappable(value); - if (Listener && (typeof value !== "function" || target.hasOwnProperty(property))) { - let nodes, node; - if (wrappable && (nodes = getDataNodes(value))) { - node = - nodes._ || - (nodes._ = "_SOLID_DEV_" - ? createSignal(undefined, false, { internal: true }) - : createSignal()); - node[0](); - } - nodes = getDataNodes(target); - node = - nodes[property] || - (nodes[property] = createResourceNode(value, `${options.name}:${property as string}`)); - node[0](); - } - return wrappable - ? wrap(value, "_SOLID_DEV_" && options.name && `${options.name}:${property as string}`) - : value; - }, - - set() { - return true; - }, - - deleteProperty() { - return true; - }, - getOwnPropertyDescriptor: proxyDescriptor - }; - - const unwrappedState = unwrap(state || {}, true), - wrappedState = wrap( - unwrappedState as any, - "_SOLID_DEV_" && ((options && options.name) || hashValue(unwrappedState)), - true, - resourceTraps - ); - if ("_SOLID_DEV_") { - const name = (options && options.name) || hashValue(unwrappedState); - registerGraph(name, { value: unwrappedState }); - } - function setState(...args: any[]): void { - batch(() => updatePath(unwrappedState, args)); - } - function loadState( - v: { [P in keyof T]: () => Promise | T[P] }, - r?: (v: Partial) => (state: State) => void - ) { - const nodes = getDataNodes(unwrappedState), - keys = Object.keys(v); - for (let i = 0; i < keys.length; i++) { - const k = keys[i], - node = - nodes[k] || (nodes[k] = createResourceNode(unwrappedState[k], `${options.name}:${k}`)), - resolver = (v?: T[keyof T]) => ( - r - ? setState(k, r(v as Partial)) - : setProperty(unwrappedState, k as string | number, v), - unwrappedState[k] - ); - node[2](() => { - const p = v[k](); - return typeof p === "object" && "then" in p ? p.then(resolver) : resolver(p); - }); - } - } - - return [wrappedState, loadState as LoadStateFunction, setState]; -} diff --git a/packages/solid/src/reactive/signal.ts b/packages/solid/src/reactive/signal.ts index 8c71f6db3..48b3560ea 100644 --- a/packages/solid/src/reactive/signal.ts +++ b/packages/solid/src/reactive/signal.ts @@ -1,5 +1,6 @@ // Inspired by S.js[https://github.com/adamhaile/S] by Adam Haile import { requestCallback, Task } from "./scheduler"; +import { sharedConfig } from "../render/hydration"; import type { JSX } from "../jsx"; export const equalFn = (a: T, b: T) => a === b; @@ -15,7 +16,8 @@ const UNOWNED: Owner = { context: null, owner: null }; -const [transPending, setTransPending] = createSignal(false, true); +export const $LAZY = Symbol("lazy"); +const [transPending, setTransPending] = /*@__PURE__*/ createSignal(false, true); export var Owner: Owner | null = null; export var Listener: Computation | null = null; let Pending: Signal[] | null = null; @@ -25,6 +27,10 @@ let Transition: Transition | null = null; let ExecCount = 0; let rootCount = 0; +declare global { + var _$afterUpdate: () => void; +} + interface Signal { value?: T; observers: Computation[] | null; @@ -78,11 +84,7 @@ export function createRoot(fn: (dispose: () => void) => T, detachedOwner?: Ow ? UNOWNED : { owned: null, cleanups: null, context: null, owner, attached: !!detachedOwner }; - if ("_SOLID_DEV_" && owner) - root.name = - (owner as Computation).name + - "-r" + - rootCount++; + if ("_SOLID_DEV_" && owner) root.name = (owner as Computation).name + "-r" + rootCount++; Owner = root; Listener = null; let result: T; @@ -135,7 +137,6 @@ export function createRenderEffect(fn: (v?: T) => T, value?: T): void { export function createEffect(fn: (v: T) => T, value: T): void; export function createEffect(fn: (v?: T) => T | undefined): void; export function createEffect(fn: (v?: T) => T, value?: T): void { - if (globalThis._$HYDRATION && globalThis._$HYDRATION.asyncSSR) return; runEffects = runUserEffects; const c = createComputation(fn, value, false), s = SuspenseContext && lookup(Owner, SuspenseContext.id); @@ -275,9 +276,9 @@ export function untrack(fn: () => T): T { return result; } -type ReturnTypeArray = { [P in keyof T]: T[P] extends (() => infer U) ? U : never }; +type ReturnTypeArray = { [P in keyof T]: T[P] extends () => infer U ? U : never }; export function on T>, U>( - ...args: X['length'] extends 1 + ...args: X["length"] extends 1 ? [w: () => T, fn: (v: T, prev: T | undefined, prevResults?: U) => U] : [...w: X, fn: (v: ReturnTypeArray, prev: ReturnTypeArray | [], prevResults?: U) => U] ): (prev?: U) => U { @@ -329,18 +330,18 @@ export function getListener() { return Listener; } -export function getContextOwner() { +export function getOwner() { return Owner; } -export function runWithOwner(owner: Owner | null, callback: () => T) { - const currentOwner = getContextOwner(); - - Owner = owner; - const result = callback(); - Owner = currentOwner; - - return result; +export function runWithOwner(o: Owner, fn: () => any) { + const prev = Owner; + Owner = o; + try { + return fn(); + } finally { + Owner = prev; + } } export function hashValue(v: any) { @@ -376,8 +377,8 @@ export interface Context { defaultValue: T; } -export function createContext(): Context -export function createContext(defaultValue: T): Context +export function createContext(): Context; +export function createContext(defaultValue: T): Context; export function createContext(defaultValue?: T): Context { const id = Symbol("context"); return { id, Provider: createProvider(id), defaultValue }; @@ -387,6 +388,11 @@ export function useContext(context: Context): T { return lookup(Owner, context.id) || context.defaultValue; } +export function children(fn: () => any) { + const children = createMemo(fn); + return createMemo(() => resolveChildren(children())); +} + // Resource API type SuspenseContextType = { increment?: () => void; @@ -410,19 +416,30 @@ export interface Resource { (): T | undefined; loading: boolean; } -export function createResource( - init?: T, - options: { name?: string; notStreamed?: boolean } = {} -): [Resource, (fn: () => Promise | T) => Promise] { +export function createResource( + key: U | false | (() => U | false), + fetcher: (k: U, getPrev: () => T | undefined) => T | Promise, + init?: T +): [ + Resource, + { + mutate: (v: T | undefined) => T | undefined; + refetch: () => void; + } +] { const contexts = new Set(), - h = globalThis._$HYDRATION || {}, [s, set] = createSignal(init, true), [track, trigger] = createSignal(), [loading, setLoading] = createSignal(false, true); let err: any = null, pr: Promise | null = null, - ctx: any; + id: string | null = null, + dynamic = typeof key === "function"; + + if (sharedConfig.context) { + id = `${sharedConfig.context!.id}${sharedConfig.context!.count++}`; + } function loadEnd(p: Promise | null, v: T, e?: any) { if (pr === p) { err = e; @@ -443,14 +460,11 @@ export function createResource( } function completeLoad(v: T) { batch(() => { - if (ctx) h.context = ctx; - if (h.asyncSSR && options.name) h.resources![options.name] = v; set(v); setLoading(false); for (let c of contexts.keys()) c.decrement!(); contexts.clear(); }); - if (ctx) h.context = ctx = undefined; } function read() { @@ -471,32 +485,33 @@ export function createResource( } return v; } - function load(fn: () => Promise | T) { + function load() { err = null; - let p: Promise | T; - const hydrating = h.context && !!h.context.registry; - if (hydrating) { - if (h.loadResource && !options.notStreamed) { - fn = h.loadResource; - } else if (options.name && h.resources && options.name in h.resources) { - fn = () => { - const data = h.resources![options.name!]; - delete h.resources![options.name!]; - return data; - }; + let p: Promise | T, + lookup = dynamic ? (key as () => U)() : (key as U); + if (!lookup) { + loadEnd(pr, untrack(s)!); + return; + } + if (sharedConfig.context) { + if (sharedConfig.context.loadResource && lookup !== ($LAZY as any)) { + p = sharedConfig.context.loadResource!(id!); + } else if (sharedConfig.resources && id && id in sharedConfig.resources) { + p = sharedConfig.resources![id]; + delete sharedConfig.resources![id]; } - } else if (h.asyncSSR && h.context) ctx = h.context; - p = fn(); + } + if (!p!) p = fetcher(lookup, s); if (typeof p !== "object" || !("then" in p)) { loadEnd(pr, p); - return Promise.resolve(p); + return; } pr = p; batch(() => { setLoading(true); trigger(); }); - return p.then( + p.then( v => loadEnd(p as Promise, v), e => loadEnd(p as Promise, undefined as any, e) ); @@ -506,7 +521,9 @@ export function createResource( return loading(); } }); - return [read as Resource, load]; + if (dynamic) createComputed(load); + else load(); + return [read as Resource, { refetch: load, mutate: set }]; } function readSignal(this: Signal | Memo) { @@ -693,45 +710,49 @@ function runUpdates(fn: () => void, init: boolean) { } catch (err) { handleError(err); } finally { - if (Updates) { - runQueue(Updates); - Updates = null; - } - if (wait) return; - if (Transition && Transition.running) { - if (Transition.promises.size) { - Transition.running = false; - Transition.effects.push.apply(Transition.effects, Effects); - Effects = null; - setTransPending(true); - return; - } - // finish transition - const sources = Transition.sources; - Transition = null; - batch(() => { - sources.forEach(v => { - v.value = v.tValue; - if ((v as Memo).owned) { - for (let i = 0, len = (v as Memo).owned!.length; i < len; i++) - cleanNode((v as Memo).owned![i]); - } - if ((v as Memo).tOwned) (v as Memo).owned = (v as Memo).tOwned!; - delete v.tValue; - delete (v as Memo).tOwned; - }); - setTransPending(false); - }); + completeUpdates(wait); + } +} + +function completeUpdates(wait: boolean) { + if (Updates) { + runQueue(Updates); + Updates = null; + } + if (wait) return; + if (Transition && Transition.running) { + if (Transition.promises.size) { + Transition.running = false; + Transition.effects.push.apply(Transition.effects, Effects!); + Effects = null; + setTransPending(true); + return; } - if (Effects.length) - batch(() => { - runEffects(Effects!); - Effects = null; + // finish transition + const sources = Transition.sources; + Transition = null; + batch(() => { + sources.forEach(v => { + v.value = v.tValue; + if ((v as Memo).owned) { + for (let i = 0, len = (v as Memo).owned!.length; i < len; i++) + cleanNode((v as Memo).owned![i]); + } + if ((v as Memo).tOwned) (v as Memo).owned = (v as Memo).tOwned!; + delete v.tValue; + delete (v as Memo).tOwned; }); - else { + setTransPending(false); + }); + } + if (Effects!.length) + batch(() => { + runEffects(Effects!); Effects = null; - if ("_SOLID_DEV_") globalThis._$afterUpdate && globalThis._$afterUpdate(); - } + }); + else { + Effects = null; + if ("_SOLID_DEV_") globalThis._$afterUpdate && globalThis._$afterUpdate(); } } @@ -834,7 +855,7 @@ function lookup(owner: Owner | null, key: symbol | string): any { ); } -function resolveChildren(children: any): any { +function resolveChildren(children: any): unknown { if (typeof children === "function") return resolveChildren(children()); if (Array.isArray(children)) { const results: any[] = []; @@ -851,8 +872,7 @@ function createProvider(id: symbol) { return function provider(props: { value: unknown; children: any }) { return (createMemo(() => { Owner!.context = { [id]: props.value }; - const children = createMemo(() => props.children); - return createMemo(() => resolveChildren(children())); + return children(() => props.children); }) as unknown) as JSX.Element; }; } diff --git a/packages/solid/src/reactive/state.ts b/packages/solid/src/reactive/state.ts index 36c0dfadf..9f8a26da5 100644 --- a/packages/solid/src/reactive/state.ts +++ b/packages/solid/src/reactive/state.ts @@ -1,4 +1,4 @@ -import { Listener, createSignal, createMemo, batch, hashValue, registerGraph } from "./signal"; +import { Listener, createSignal, batch, hashValue, registerGraph } from "./signal"; export const $RAW = Symbol("state-raw"), $NODE = Symbol("state-node"), $PROXY = Symbol("state-proxy"), @@ -35,35 +35,10 @@ export type State = { AddSymbolToStringTag & AddCallable; -export function wrap( - value: T, - name?: string, - processProps?: boolean, - traps?: ProxyHandler -): State { +function wrap(value: T, name?: string): State { let p = value[$PROXY]; if (!p) { - Object.defineProperty(value, $PROXY, { value: (p = new Proxy(value, traps || proxyTraps)) }); - if (processProps) { - let keys = Object.keys(value), - desc = Object.getOwnPropertyDescriptors(value); - for (let i = 0, l = keys.length; i < l; i++) { - const prop = keys[i]; - if (desc[prop].get) { - const get = createMemo(desc[prop].get!.bind(p), undefined, true); - Object.defineProperty(value, prop, { - get - }); - } - if (desc[prop].set) { - const og = desc[prop].set!, - set = (v: T[keyof T]) => batch(() => og.call(p, v)); - Object.defineProperty(value, prop, { - set - }); - } - } - } + Object.defineProperty(value, $PROXY, { value: (p = new Proxy(value, proxyTraps)) }); if ("_SOLID_DEV_" && name) Object.defineProperty(value, $NAME, { value: name }); } return p; @@ -77,7 +52,7 @@ export function isWrappable(obj: any) { ); } -export function unwrap(item: any, skipGetters?: boolean): T { +export function unwrap(item: any): T { let result, unwrapped, v, prop; if ((result = item != null && item[$RAW])) return result; if (!isWrappable(item)) return item; @@ -86,17 +61,17 @@ export function unwrap(item: any, skipGetters?: boolean): T if (Object.isFrozen(item)) item = item.slice(0); for (let i = 0, l = item.length; i < l; i++) { v = item[i]; - if ((unwrapped = unwrap(v, skipGetters)) !== v) item[i] = unwrapped; + if ((unwrapped = unwrap(v)) !== v) item[i] = unwrapped; } } else { if (Object.isFrozen(item)) item = Object.assign({}, item); let keys = Object.keys(item), - desc = skipGetters && Object.getOwnPropertyDescriptors(item); + desc = Object.getOwnPropertyDescriptors(item); for (let i = 0, l = keys.length; i < l; i++) { prop = keys[i]; - if (skipGetters && (desc as any)[prop].get) continue; + if ((desc as any)[prop].get) continue; v = item[prop]; - if ((unwrapped = unwrap(v, skipGetters)) !== v) item[prop] = unwrapped; + if ((unwrapped = unwrap(v)) !== v) item[prop] = unwrapped; } } return item; @@ -114,7 +89,7 @@ export function proxyDescriptor(target: StateNode, property: string | number | s return desc; delete desc.value; delete desc.writable; - desc.get = () => target[property as string | number]; + desc.get = () => target[$PROXY][property as string | number]; return desc; } @@ -160,11 +135,7 @@ const proxyTraps: ProxyHandler = { getOwnPropertyDescriptor: proxyDescriptor }; -export function setProperty( - state: StateNode, - property: string | number, - value: any -) { +export function setProperty(state: StateNode, property: string | number, value: any) { if (state[property] === value) return; const notify = Array.isArray(state) || !(property in state); if (value === undefined) { @@ -337,11 +308,10 @@ export function createState( state: T | State, options?: { name?: string } ): [State, SetStateFunction] { - const unwrappedState = unwrap(state || {}, true); + const unwrappedState = unwrap(state || {}); const wrappedState = wrap( unwrappedState, - "_SOLID_DEV_" && ((options && options.name) || hashValue(unwrappedState)), - true + "_SOLID_DEV_" && ((options && options.name) || hashValue(unwrappedState)) ); if ("_SOLID_DEV_") { const name = (options && options.name) || hashValue(unwrappedState); diff --git a/packages/solid/src/reactive/stateModifiers.ts b/packages/solid/src/reactive/stateModifiers.ts index 06c788a8e..115747e6a 100644 --- a/packages/solid/src/reactive/stateModifiers.ts +++ b/packages/solid/src/reactive/stateModifiers.ts @@ -110,7 +110,7 @@ export function reconcile( const { merge, key = "id" } = options, v = unwrap(value); return state => { - if (!isWrappable(state)) return v as T extends NotWrappable ? T : State; + if (!isWrappable(state) || !isWrappable(v)) return v as T extends NotWrappable ? T : State; applyState(v, { state }, "state", merge, key); return state; }; diff --git a/packages/solid/src/render/Suspense.ts b/packages/solid/src/render/Suspense.ts index 4682f9f38..ba5824ef5 100644 --- a/packages/solid/src/render/Suspense.ts +++ b/packages/solid/src/render/Suspense.ts @@ -2,7 +2,6 @@ import { createComponent } from "./component"; import { createSignal, untrack, - createRenderEffect, createComputed, createContext, useContext, @@ -23,24 +22,6 @@ type SuspenseListContextType = { }; const SuspenseListContext = createContext(); -let trackSuspense = false; -export function awaitSuspense(fn: () => any) { - const SuspenseContext = getSuspenseContext(); - if (!trackSuspense) { - let count = 0; - const [active, trigger] = createSignal(false); - SuspenseContext.active = active; - SuspenseContext.increment = () => count++ === 0 && trigger(true); - SuspenseContext.decrement = () => --count <= 0 && trigger(false); - trackSuspense = true; - } - return () => - new Promise(resolve => { - const res = fn(); - createRenderEffect(() => !SuspenseContext.active!() && resolve(res)); - }); -} - export function SuspenseList(props: { children: JSX.Element; revealOrder: "forwards" | "backwards" | "together"; @@ -122,16 +103,10 @@ export function Suspense(props: { fallback: JSX.Element; children: JSX.Element } SuspenseContext = getSuspenseContext(), store = { increment: () => { - if (++counter === 1) { - setFallback(true); - trackSuspense && SuspenseContext.increment!(); - } + if (++counter === 1) setFallback(true); }, decrement: () => { - if (--counter === 0) { - setFallback(false); - trackSuspense && setTimeout(SuspenseContext.decrement!); - } + if (--counter === 0) setFallback(false); }, inFallback, effects: [], diff --git a/packages/solid/src/render/component.ts b/packages/solid/src/render/component.ts index 6b3879fc5..3e9536c7e 100644 --- a/packages/solid/src/render/component.ts +++ b/packages/solid/src/render/component.ts @@ -1,5 +1,6 @@ -import { untrack, createResource, createMemo } from "../reactive/signal"; -import type { JSX } from "../jsx" +import { untrack, createSignal, createResource, createMemo, $LAZY } from "../reactive/signal"; +import { sharedConfig, nextHydrateContext, setHydrateContext } from "./hydration"; +import type { JSX } from "../jsx"; type PropsWithChildren

= P & { children?: JSX.Element }; export type Component

= (props: PropsWithChildren

) => JSX.Element; @@ -18,23 +19,63 @@ export type ComponentProps< ? JSX.IntrinsicElements[T] : {}; export function createComponent(Comp: (props: T) => JSX.Element, props: T): JSX.Element { + if (sharedConfig.context) { + const c = sharedConfig.context; + setHydrateContext(nextHydrateContext()); + const r = untrack(() => Comp(props as T)); + setHydrateContext(c); + return r; + } return untrack(() => Comp(props as T)); } -export function assignProps(target: T, source: U): T & U; -export function assignProps(target: T, source1: U, source2: V): T & U & V; -export function assignProps( - target: T, +const propTraps: ProxyHandler<{ get: (k: string | number | symbol) => any; keys: () => any }> = { + get(_, property) { + return _.get(property); + }, + has(_, property) { + return _.get(property) !== undefined; + }, + getOwnPropertyDescriptor(_, property) { + return { + configurable: true, + enumerable: true, + get() { + return _.get(property); + } + }; + }, + ownKeys(_) { + return _.keys(); + } +}; + +export function mergeProps(source: T): T; +export function mergeProps(source: T, source1: U): T & U; +export function mergeProps(source: T, source1: U, source2: V): T & U & V; +export function mergeProps( + source: T, source1: U, source2: V, source3: W ): T & U & V & W; -export function assignProps(target: any, ...sources: any): any { - for (let i = 0; i < sources.length; i++) { - const descriptors = Object.getOwnPropertyDescriptors(sources[i]); - Object.defineProperties(target, descriptors); - } - return target; +export function mergeProps(...sources: any): any { + return new Proxy( + { + get(property: string | number | symbol) { + for (let i = sources.length - 1; i >= 0; i--) { + const v = sources[i][property]; + if (v !== undefined) return v; + } + }, + keys() { + const keys = []; + for (let i = 0; i < sources.length; i++) keys.push(...Object.keys(sources[i])); + return [...new Set(keys)]; + } + }, + propTraps + ); } export function splitProps( @@ -82,44 +123,60 @@ export function splitProps< Pick, Omit ]; -export function splitProps(props: T, ...keys: [(keyof T)[]]) { - const descriptors = Object.getOwnPropertyDescriptors(props), - split = (k: (keyof T)[]) => { - const clone: Partial = {}; - for (let i = 0; i < k.length; i++) { - const key = k[i]; - if (descriptors[key]) { - Object.defineProperty(clone, key, descriptors[key]); - delete descriptors[key]; +export function splitProps(props: T, ...keys: Array<(keyof T)[]>) { + const blocked = new Set(keys.flat()); + const descriptors = Object.getOwnPropertyDescriptors(props); + const res = keys.map(k => { + const clone = {}; + for (let i = 0; i < k.length; i++) { + const key = k[i]; + if (descriptors[key]) Object.defineProperty(clone, key, descriptors[key]); + } + return clone; + }); + res.push( + new Proxy( + { + get(property: string | number | symbol) { + if (blocked.has(property as keyof T)) return; + return props[property as keyof T]; + }, + keys() { + return Object.keys(props).filter(k => !blocked.has(k as keyof T)); } - } - return clone; - }; - return keys.map(split).concat(split(Object.keys(descriptors) as (keyof T)[])); + }, + propTraps + ) + ); + return res; } // lazy load a function component asynchronously export function lazy>(fn: () => Promise<{ default: T }>): T { let p: Promise<{ default: T }>; return ((props: any) => { - const h = globalThis._$HYDRATION || {}, - hydrating = h.context && h.context.registry, - ctx = nextHydrateContext(), - [s, l] = createResource(undefined, { notStreamed: true }); - if (hydrating && h.resources) { + const ctx = sharedConfig.context; + let comp: () => T | undefined; + if (ctx && sharedConfig.resources) { + ctx.count++; // increment counter for hydration + const [s, set] = createSignal(); (p || (p = fn())).then(mod => { setHydrateContext(ctx); - l(() => mod.default); + set(mod.default); setHydrateContext(undefined); }); - } else l(() => (p || (p = fn())).then(mod => mod.default)); + comp = s; + } else { + const [s] = createResource($LAZY, () => (p || (p = fn())).then(mod => mod.default)); + comp = s; + } let Comp: T | undefined; return createMemo( () => - (Comp = s()) && + (Comp = comp()) && untrack(() => { if (!ctx) return Comp!(props); - const c = h.context; + const c = sharedConfig.context; setHydrateContext(ctx); const r = Comp!(props); setHydrateContext(c); @@ -128,38 +185,3 @@ export function lazy>(fn: () => Promise<{ default: T }> ); }) as T; } - -function setHydrateContext(context?: HydrationContext): void { - globalThis._$HYDRATION.context = context; -} - -function nextHydrateContext(): HydrationContext | undefined { - const hydration = globalThis._$HYDRATION; - return hydration && hydration.context - ? { - id: `${hydration.context.id}.${hydration.context.count++}`, - count: 0, - registry: hydration.context.registry - } - : undefined; -} - -type HydrationContext = { - id: string; - count: number; - registry?: Map; -}; - -type GlobalHydration = { - context?: HydrationContext; - register?: (v: Promise) => void; - loadResource?: () => Promise; - resources?: { [key: string]: any }; - asyncSSR?: boolean; - streamSSR?: boolean; -}; - -declare global { - var _$HYDRATION: GlobalHydration; - var _$afterUpdate: () => void; -} diff --git a/packages/solid/src/render/flow.ts b/packages/solid/src/render/flow.ts index 968e0ad98..0844b795e 100644 --- a/packages/solid/src/render/flow.ts +++ b/packages/solid/src/render/flow.ts @@ -1,4 +1,4 @@ -import { createMemo, untrack, createSignal, onError } from "../reactive/signal"; +import { createMemo, untrack, createSignal, onError, children } from "../reactive/signal"; import { mapArray, indexArray } from "../reactive/array"; import type { JSX } from "../jsx"; @@ -48,23 +48,28 @@ export function Show(props: { } export function Switch(props: { fallback?: JSX.Element; children: JSX.Element }) { - let conditions = (props.children as unknown) as (MatchProps & { keyed: boolean })[]; - Array.isArray(conditions) || (conditions = [conditions]); - const evalConditions = createMemo<[number, unknown?]>( + let conditions = children(() => props.children) as () => (MatchProps & { + keyed: boolean; + })[]; + const evalConditions = createMemo< + [number, unknown?, (MatchProps & { keyed: boolean })?] + >( () => { - for (let i = 0; i < conditions.length; i++) { - const c = conditions[i].when; - if (c) return [i, conditions[i].keyed ? c : !!c]; + let conds = conditions(); + if (!Array.isArray(conds)) conds = [conds]; + for (let i = 0; i < conds.length; i++) { + const c = conds[i].when; + if (c) return [i, conds[i].keyed ? c : !!c, conds[i]]; } return [-1]; }, undefined, - (a: [number, unknown?], b: [number, unknown?]) => a && a[0] === b[0] && a[1] === b[1] + (a: [number, unknown?, unknown?], b: [number, unknown?, unknown?]) => a && a[0] === b[0] && a[1] === b[1] && a[2] === b[2] ); return createMemo(() => { - const [index, when] = evalConditions(); + const [index, when, cond] = evalConditions(); if (index < 0) return props.fallback; - const c = conditions[index].children; + const c = cond!.children; return typeof c === "function" && c.length ? untrack(() => c(when)) : (c as JSX.Element); }); } diff --git a/packages/solid/src/render/hydration.ts b/packages/solid/src/render/hydration.ts new file mode 100644 index 000000000..117c8f963 --- /dev/null +++ b/packages/solid/src/render/hydration.ts @@ -0,0 +1,21 @@ +type HydrationContext = { id: string; count: number; loadResource?: (id: string) => Promise }; + +type SharedConfig = { + context?: HydrationContext; + resources?: { [key: string]: any }; + registry?: Map; +}; + +export const sharedConfig: SharedConfig = {}; + +export function setHydrateContext(context?: HydrationContext): void { + sharedConfig.context = context; +} + +export function nextHydrateContext(): HydrationContext | undefined { + return { + ...sharedConfig.context, + id: `${sharedConfig.context!.id}${sharedConfig.context!.count++}.`, + count: 0 + }; +} diff --git a/packages/solid/src/render/index.ts b/packages/solid/src/render/index.ts index 87c753772..fb9ee6f8a 100644 --- a/packages/solid/src/render/index.ts +++ b/packages/solid/src/render/index.ts @@ -1,3 +1,4 @@ export * from "./component"; export * from "./flow"; -export * from "./Suspense" \ No newline at end of file +export * from "./Suspense" +export { sharedConfig } from "./hydration"; \ No newline at end of file diff --git a/packages/solid/src/static/index.ts b/packages/solid/src/static/index.ts index ba327cfe9..422452dd0 100644 --- a/packages/solid/src/static/index.ts +++ b/packages/solid/src/static/index.ts @@ -1,53 +1,54 @@ -export { - createRoot, - createSignal, - createComputed, - createRenderEffect, - createEffect, - createDeferred, - createSelector, - createMemo, - getListener, - onMount, - onCleanup, - onError, - untrack, - batch, - on, - createContext, - useContext, - getContextOwner, - runWithOwner, - equalFn, - requestCallback, - createState, - unwrap, - $RAW, - reconcile, - produce, - mapArray -} from "./reactive"; - -export { - assignProps, - splitProps, - createComponent, - For, - Index, - Show, - Switch, - Match, - ErrorBoundary, - Suspense, - SuspenseList, - createResource, - createResourceState, - useTransition, - lazy -} from "./rendering"; - -export type { State, SetStateFunction } from "./reactive"; -export type { Component, LoadStateFunction, Resource } from "./rendering"; - - - +export { + createRoot, + createSignal, + createComputed, + createRenderEffect, + createEffect, + createDeferred, + createSelector, + createMemo, + getListener, + onMount, + onCleanup, + onError, + untrack, + batch, + on, + createContext, + useContext, + getOwner, + runWithOwner, + equalFn, + requestCallback, + createState, + unwrap, + $RAW, + reconcile, + produce, + mapArray +} from "./reactive"; + +export { + awaitSuspense, + mergeProps, + splitProps, + createComponent, + For, + Index, + Show, + Switch, + Match, + ErrorBoundary, + Suspense, + SuspenseList, + createResource, + useTransition, + lazy, + sharedConfig +} from "./rendering"; + +export type { State, SetStateFunction } from "./reactive"; +export type { Component, Resource } from "./rendering"; + + + diff --git a/packages/solid/src/static/reactive.ts b/packages/solid/src/static/reactive.ts index c315e4121..035da76f0 100644 --- a/packages/solid/src/static/reactive.ts +++ b/packages/solid/src/static/reactive.ts @@ -1,438 +1,438 @@ -export const equalFn = (a: T, b: T) => a === b; -const ERROR = Symbol("error"); - -const UNOWNED: Owner = { context: null, owner: null }; -let Owner: Owner | null = null; - -interface Owner { - owner: Owner | null; - context: any | null; -} - -export function createRoot(fn: (dispose: () => void) => T, detachedOwner?: Owner): T { - detachedOwner && (Owner = detachedOwner); - const owner = Owner, - root: Owner = fn.length === 0 ? UNOWNED : { context: null, owner }; - Owner = root; - let result: T; - try { - result = fn(() => {}); - } catch (err) { - const fns = lookup(Owner, ERROR); - if (!fns) throw err; - fns.forEach((f: (err: any) => void) => f(err)); - } finally { - Owner = owner; - } - return result!; -} - -export function createSignal( - value?: T, - areEqual?: boolean | ((prev: T, next: T) => boolean) -): [() => T, (v: T) => T] { - return [() => value as T, (v: T) => (value = v)]; -} - -export function createComputed(fn: (v?: T) => T, value?: T): void { - Owner = { owner: Owner, context: null }; - fn(value); - Owner = Owner.owner; -} - -export function createRenderEffect(fn: (v?: T) => T, value?: T): void { - Owner = { owner: Owner, context: null }; - fn(value); - Owner = Owner.owner; -} - -export function createEffect(fn: (v?: T) => T, value?: T): void {} - -export function createMemo( - fn: (v?: T) => T, - value?: T, - areEqual?: boolean | ((prev: T, next: T) => boolean) -): () => T { - Owner = { owner: Owner, context: null }; - const v = fn(value); - Owner = Owner.owner; - return () => v; -} - -export function createDeferred(source: () => T, options?: { timeoutMs: number }) { - return source; -} - -export function createSelector( - source: () => T, - fn: (k: T, value: T, prevValue: T | undefined) => boolean -) { - return source; -} - -export function batch(fn: () => T): T { - return fn(); -} - -export function untrack(fn: () => T): T { - return fn(); -} - -type ReturnTypeArray = { [P in keyof T]: T[P] extends (() => infer U) ? U : never }; -export function on T>, U>( - ...args: X['length'] extends 1 - ? [w: () => T, fn: (v: T, prev: T | undefined, prevResults?: U) => U] - : [...w: X, fn: (v: ReturnTypeArray, prev: ReturnTypeArray | [], prevResults?: U) => U] -): (prev?: U) => U { - const fn = args.pop() as (v: T | Array, p?: T | Array, r?: U) => U; - let deps: (() => T) | Array<() => T>; - let isArray = true; - let prev: T | T[]; - if (args.length < 2) { - deps = args[0] as () => T; - isArray = false; - } else deps = args as Array<() => T>; - return prevResult => { - let value: T | Array; - if (isArray) { - value = []; - if (!prev) prev = []; - for (let i = 0; i < deps.length; i++) value.push((deps as Array<() => T>)[i]()); - } else value = (deps as () => T)(); - return fn!(value, prev, prevResult); - }; -} - -export function onMount(fn: () => void) {} - -export function onCleanup(fn: () => void) {} - -export function onError(fn: (err: any) => void): void { - if (Owner === null) - "_SOLID_DEV_" && - console.warn("error handlers created outside a `createRoot` or `render` will never be run"); - else if (Owner.context === null) Owner.context = { [ERROR]: [fn] }; - else if (!Owner.context[ERROR]) Owner.context[ERROR] = [fn]; - else Owner.context[ERROR].push(fn); -} - -export function getListener() { - return null; -} - -// Context API -export interface Context { - id: symbol; - Provider: (props: { value: T; children: any }) => any; - defaultValue?: T; -} - -export function createContext(defaultValue?: T): Context { - const id = Symbol("context"); - return { id, Provider: createProvider(id), defaultValue }; -} - -export function useContext(context: Context): T { - return lookup(Owner, context.id) || context.defaultValue; -} - -export function getContextOwner() { - return Owner; -} - -export function runWithOwner(owner: Owner | null, callback: () => T) { - const currentOwner = getContextOwner(); - - Owner = owner; - const result = callback(); - Owner = currentOwner; - - return result; -} - -function lookup(owner: Owner | null, key: symbol | string): any { - return ( - owner && ((owner.context && owner.context[key]) || (owner.owner && lookup(owner.owner, key))) - ); -} - -function resolveChildren(children: any): any { - if (typeof children === "function") return resolveChildren(children()); - if (Array.isArray(children)) { - const results: any[] = []; - for (let i = 0; i < children.length; i++) { - let result = resolveChildren(children[i]); - Array.isArray(result) ? results.push.apply(results, result) : results.push(result); - } - return results; - } - return children; -} - -function createProvider(id: symbol) { - return function provider(props: { value: unknown; children: any }) { - let rendered; - createRenderEffect(() => { - Owner!.context = { [id]: props.value }; - rendered = resolveChildren(props.children); - }); - return rendered; - }; -} - -export interface Task { - id: number; - fn: ((didTimeout: boolean) => void) | null; - startTime: number; - expirationTime: number; -} -export function requestCallback(fn: () => void, options?: { timeout: number }): Task { - return { id: 0, fn: () => {}, startTime: 0, expirationTime: 0 }; -} -export function cancelCallback(task: Task) {} - -export const $RAW = Symbol("state-raw"); - -// well-known symbols need special treatment until https://github.com/microsoft/TypeScript/issues/24622 is implemented. -type AddSymbolToPrimitive = T extends { [Symbol.toPrimitive]: infer V } - ? { [Symbol.toPrimitive]: V } - : {}; -type AddCallable = T extends { (...x: any[]): infer V } ? { (...x: Parameters): V } : {}; - -type NotWrappable = string | number | boolean | Function | null; -export type State = { - [P in keyof T]: T[P] extends object ? State : T[P]; -} & { - [$RAW]?: T; -} & AddSymbolToPrimitive & - AddCallable; - -export function isWrappable(obj: any) { - return ( - obj != null && - typeof obj === "object" && - (obj.__proto__ === Object.prototype || Array.isArray(obj)) - ); -} - -export function unwrap(item: any): T { - return item; -} - -export function setProperty(state: any, property: string | number, value: any, force?: boolean) { - if (!force && state[property] === value) return; - if (value === undefined) { - delete state[property]; - } else state[property] = value; -} - -function mergeState(state: any, value: any, force?: boolean) { - const keys = Object.keys(value); - for (let i = 0; i < keys.length; i += 1) { - const key = keys[i]; - setProperty(state, key, value[key], force); - } -} - -export function updatePath(current: any, path: any[], traversed: (number | string)[] = []) { - let part, - next = current; - if (path.length > 1) { - part = path.shift(); - const partType = typeof part, - isArray = Array.isArray(current); - - if (Array.isArray(part)) { - // Ex. update('data', [2, 23], 'label', l => l + ' !!!'); - for (let i = 0; i < part.length; i++) { - updatePath(current, [part[i]].concat(path), [part[i]].concat(traversed)); - } - return; - } else if (isArray && partType === "function") { - // Ex. update('data', i => i.id === 42, 'label', l => l + ' !!!'); - for (let i = 0; i < current.length; i++) { - if (part(current[i], i)) - updatePath(current, [i].concat(path), ([i] as (number | string)[]).concat(traversed)); - } - return; - } else if (isArray && partType === "object") { - // Ex. update('data', { from: 3, to: 12, by: 2 }, 'label', l => l + ' !!!'); - const { from = 0, to = current.length - 1, by = 1 } = part; - for (let i = from; i <= to; i += by) { - updatePath(current, [i].concat(path), ([i] as (number | string)[]).concat(traversed)); - } - return; - } else if (path.length > 1) { - updatePath(current[part], path, [part].concat(traversed)); - return; - } - next = current[part]; - traversed = [part].concat(traversed); - } - let value = path[0]; - if (typeof value === "function") { - value = value(next, traversed); - if (value === next) return; - } - if (part === undefined && value == undefined) return; - if (part === undefined || (isWrappable(next) && isWrappable(value) && !Array.isArray(value))) { - mergeState(next, value); - } else setProperty(current, part, value); -} - -type StateSetter = - | Partial - | (( - prevState: T extends NotWrappable ? T : State, - traversed?: (string | number)[] - ) => Partial | void); -type StatePathRange = { from?: number; to?: number; by?: number }; - -type ArrayFilterFn = (item: T extends any[] ? T[number] : never, index: number) => boolean; - -type Part = keyof T | Array | StatePathRange | ArrayFilterFn; // changing this to "T extends any[] ? ArrayFilterFn : never" results in depth limit errors - -type Next = K extends keyof T - ? T[K] - : K extends Array - ? T[K[number]] - : T extends any[] - ? K extends StatePathRange - ? T[number] - : K extends ArrayFilterFn - ? T[number] - : never - : never; - -export interface SetStateFunction { - >(...args: [Setter]): void; - , Setter extends StateSetter>>(...args: [K1, Setter]): void; - < - K1 extends Part, - K2 extends Part>, - Setter extends StateSetter, K2>> - >( - ...args: [K1, K2, Setter] - ): void; - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - Setter extends StateSetter, K2>, K3>> - >( - ...args: [K1, K2, K3, Setter] - ): void; - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - K4 extends Part, K2>, K3>>, - Setter extends StateSetter, K2>, K3>, K4>> - >( - ...args: [K1, K2, K3, K4, Setter] - ): void; - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - K4 extends Part, K2>, K3>>, - K5 extends Part, K2>, K3>, K4>>, - Setter extends StateSetter, K2>, K3>, K4>, K5>> - >( - ...args: [K1, K2, K3, K4, K5, Setter] - ): void; - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - K4 extends Part, K2>, K3>>, - K5 extends Part, K2>, K3>, K4>>, - K6 extends Part, K2>, K3>, K4>, K5>>, - Setter extends StateSetter, K2>, K3>, K4>, K5>, K6>> - >( - ...args: [K1, K2, K3, K4, K5, K6, Setter] - ): void; - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - K4 extends Part, K2>, K3>>, - K5 extends Part, K2>, K3>, K4>>, - K6 extends Part, K2>, K3>, K4>, K5>>, - K7 extends Part, K2>, K3>, K4>, K5>, K6>>, - Setter extends StateSetter< - Next, K2>, K3>, K4>, K5>, K6>, K7> - > - >( - ...args: [K1, K2, K3, K4, K5, K6, K7, Setter] - ): void; - - // and here we give up on being accurate after 8 args - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - K4 extends Part, K2>, K3>>, - K5 extends Part, K2>, K3>, K4>>, - K6 extends Part, K2>, K3>, K4>, K5>>, - K7 extends Part, K2>, K3>, K4>, K5>, K6>>, - K8 extends Part, K2>, K3>, K4>, K5>, K6>, K7>> - >( - ...args: [K1, K2, K3, K4, K5, K6, K7, K8, ...(Part | StateSetter)[]] - ): void; -} - -export function createState(state: T | State): [State, SetStateFunction] { - function setState(...args: any[]): void { - updatePath(state, args); - } - return [state as State, setState]; -} - -type ReconcileOptions = { - key?: string | null; - merge?: boolean; -}; - -// Diff method for setState -export function reconcile( - value: T | State, - options: ReconcileOptions = {} -): (state: T extends NotWrappable ? T : State) => void { - return state => { - if (!isWrappable(state)) return value; - const targetKeys = Object.keys(value) as (keyof T)[]; - for (let i = 0, len = targetKeys.length; i < len; i++) { - const key = targetKeys[i]; - setProperty(state, key as string, value[key]); - } - const previousKeys = Object.keys(state) as (keyof T)[]; - for (let i = 0, len = previousKeys.length; i < len; i++) { - if (value[previousKeys[i]] === undefined) - setProperty(state, previousKeys[i] as string, undefined); - } - }; -} - -// Immer style mutation style -export function produce( - fn: (state: T) => void -): (state: T extends NotWrappable ? T : State) => T extends NotWrappable ? T : State { - return state => { - if (isWrappable(state)) fn(state as T); - return state; - }; -} - -export function mapArray( - list: () => T[], - mapFn: (v: T, i: () => number) => U, - options: { fallback?: () => any } = {} -): () => U[] { - const items = list(); - let s: U[] = []; - if (items.length) { - for (let i = 0, len = items.length; i < len; i++) s.push(mapFn(items[i], () => i)); - } else if (options.fallback) s = [options.fallback()]; - return () => s; -} +export const equalFn = (a: T, b: T) => a === b; +const ERROR = Symbol("error"); + +const UNOWNED: Owner = { context: null, owner: null }; +export let Owner: Owner | null = null; + +interface Owner { + owner: Owner | null; + context: any | null; +} + +export function createRoot(fn: (dispose: () => void) => T, detachedOwner?: Owner): T { + detachedOwner && (Owner = detachedOwner); + const owner = Owner, + root: Owner = fn.length === 0 ? UNOWNED : { context: null, owner }; + Owner = root; + let result: T; + try { + result = fn(() => {}); + } catch (err) { + const fns = lookup(Owner, ERROR); + if (!fns) throw err; + fns.forEach((f: (err: any) => void) => f(err)); + } finally { + Owner = owner; + } + return result!; +} + +export function createSignal( + value?: T, + areEqual?: boolean | ((prev: T, next: T) => boolean) +): [() => T, (v: T) => T] { + return [() => value as T, (v: T) => (value = v)]; +} + +export function createComputed(fn: (v?: T) => T, value?: T): void { + Owner = { owner: Owner, context: null }; + fn(value); + Owner = Owner.owner; +} + +export const createRenderEffect = createComputed; + +export function createEffect(fn: (v?: T) => T, value?: T): void {} + +export function createMemo( + fn: (v?: T) => T, + value?: T, + areEqual?: boolean | ((prev: T, next: T) => boolean) +): () => T { + Owner = { owner: Owner, context: null }; + const v = fn(value); + Owner = Owner.owner; + return () => v; +} + +export function createDeferred(source: () => T, options?: { timeoutMs: number }) { + return source; +} + +export function createSelector( + source: () => T, + fn: (k: T, value: T, prevValue: T | undefined) => boolean +) { + return source; +} + +export function batch(fn: () => T): T { + return fn(); +} + +export function untrack(fn: () => T): T { + return fn(); +} + +type ReturnTypeArray = { [P in keyof T]: T[P] extends (() => infer U) ? U : never }; +export function on T>, U>( + ...args: X['length'] extends 1 + ? [w: () => T, fn: (v: T, prev: T | undefined, prevResults?: U) => U] + : [...w: X, fn: (v: ReturnTypeArray, prev: ReturnTypeArray | [], prevResults?: U) => U] +): (prev?: U) => U { + const fn = args.pop() as (v: T | Array, p?: T | Array, r?: U) => U; + let deps: (() => T) | Array<() => T>; + let isArray = true; + let prev: T | T[]; + if (args.length < 2) { + deps = args[0] as () => T; + isArray = false; + } else deps = args as Array<() => T>; + return prevResult => { + let value: T | Array; + if (isArray) { + value = []; + if (!prev) prev = []; + for (let i = 0; i < deps.length; i++) value.push((deps as Array<() => T>)[i]()); + } else value = (deps as () => T)(); + return fn!(value, prev, prevResult); + }; +} + +export function onMount(fn: () => void) {} + +export function onCleanup(fn: () => void) {} + +export function onError(fn: (err: any) => void): void { + if (Owner === null) + "_SOLID_DEV_" && + console.warn("error handlers created outside a `createRoot` or `render` will never be run"); + else if (Owner.context === null) Owner.context = { [ERROR]: [fn] }; + else if (!Owner.context[ERROR]) Owner.context[ERROR] = [fn]; + else Owner.context[ERROR].push(fn); +} + +export function getListener() { + return null; +} + +// Context API +export interface Context { + id: symbol; + Provider: (props: { value: T; children: any }) => any; + defaultValue?: T; +} + +export function createContext(defaultValue?: T): Context { + const id = Symbol("context"); + return { id, Provider: createProvider(id), defaultValue }; +} + +export function useContext(context: Context): T { + return lookup(Owner, context.id) || context.defaultValue; +} + +export function getOwner() { + return Owner; +} + +export function children(fn: () => any) { + return resolveChildren(fn()) +} + +export function runWithOwner(o: Owner, fn: () => any) { + const prev = Owner; + Owner = o; + try { + return fn(); + } finally { + Owner = prev; + } +} + +export function lookup(owner: Owner | null, key: symbol | string): any { + return ( + owner && ((owner.context && owner.context[key]) || (owner.owner && lookup(owner.owner, key))) + ); +} + +function resolveChildren(children: any): any { + if (typeof children === "function") return resolveChildren(children()); + if (Array.isArray(children)) { + const results: any[] = []; + for (let i = 0; i < children.length; i++) { + let result = resolveChildren(children[i]); + Array.isArray(result) ? results.push.apply(results, result) : results.push(result); + } + return results; + } + return children; +} + +function createProvider(id: symbol) { + return function provider(props: { value: unknown; children: any }) { + let rendered; + createRenderEffect(() => { + Owner!.context = { [id]: props.value }; + rendered = resolveChildren(props.children); + }); + return rendered; + }; +} + +export interface Task { + id: number; + fn: ((didTimeout: boolean) => void) | null; + startTime: number; + expirationTime: number; +} +export function requestCallback(fn: () => void, options?: { timeout: number }): Task { + return { id: 0, fn: () => {}, startTime: 0, expirationTime: 0 }; +} +export function cancelCallback(task: Task) {} + +export const $RAW = Symbol("state-raw"); + +// well-known symbols need special treatment until https://github.com/microsoft/TypeScript/issues/24622 is implemented. +type AddSymbolToPrimitive = T extends { [Symbol.toPrimitive]: infer V } + ? { [Symbol.toPrimitive]: V } + : {}; +type AddCallable = T extends { (...x: any[]): infer V } ? { (...x: Parameters): V } : {}; + +type NotWrappable = string | number | boolean | Function | null; +export type State = { + [P in keyof T]: T[P] extends object ? State : T[P]; +} & { + [$RAW]?: T; +} & AddSymbolToPrimitive & + AddCallable; + +export function isWrappable(obj: any) { + return ( + obj != null && + typeof obj === "object" && + (obj.__proto__ === Object.prototype || Array.isArray(obj)) + ); +} + +export function unwrap(item: any): T { + return item; +} + +export function setProperty(state: any, property: string | number, value: any, force?: boolean) { + if (!force && state[property] === value) return; + if (value === undefined) { + delete state[property]; + } else state[property] = value; +} + +function mergeState(state: any, value: any, force?: boolean) { + const keys = Object.keys(value); + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + setProperty(state, key, value[key], force); + } +} + +export function updatePath(current: any, path: any[], traversed: (number | string)[] = []) { + let part, + next = current; + if (path.length > 1) { + part = path.shift(); + const partType = typeof part, + isArray = Array.isArray(current); + + if (Array.isArray(part)) { + // Ex. update('data', [2, 23], 'label', l => l + ' !!!'); + for (let i = 0; i < part.length; i++) { + updatePath(current, [part[i]].concat(path), [part[i]].concat(traversed)); + } + return; + } else if (isArray && partType === "function") { + // Ex. update('data', i => i.id === 42, 'label', l => l + ' !!!'); + for (let i = 0; i < current.length; i++) { + if (part(current[i], i)) + updatePath(current, [i].concat(path), ([i] as (number | string)[]).concat(traversed)); + } + return; + } else if (isArray && partType === "object") { + // Ex. update('data', { from: 3, to: 12, by: 2 }, 'label', l => l + ' !!!'); + const { from = 0, to = current.length - 1, by = 1 } = part; + for (let i = from; i <= to; i += by) { + updatePath(current, [i].concat(path), ([i] as (number | string)[]).concat(traversed)); + } + return; + } else if (path.length > 1) { + updatePath(current[part], path, [part].concat(traversed)); + return; + } + next = current[part]; + traversed = [part].concat(traversed); + } + let value = path[0]; + if (typeof value === "function") { + value = value(next, traversed); + if (value === next) return; + } + if (part === undefined && value == undefined) return; + if (part === undefined || (isWrappable(next) && isWrappable(value) && !Array.isArray(value))) { + mergeState(next, value); + } else setProperty(current, part, value); +} + +type StateSetter = + | Partial + | (( + prevState: T extends NotWrappable ? T : State, + traversed?: (string | number)[] + ) => Partial | void); +type StatePathRange = { from?: number; to?: number; by?: number }; + +type ArrayFilterFn = (item: T extends any[] ? T[number] : never, index: number) => boolean; + +type Part = keyof T | Array | StatePathRange | ArrayFilterFn; // changing this to "T extends any[] ? ArrayFilterFn : never" results in depth limit errors + +type Next = K extends keyof T + ? T[K] + : K extends Array + ? T[K[number]] + : T extends any[] + ? K extends StatePathRange + ? T[number] + : K extends ArrayFilterFn + ? T[number] + : never + : never; + +export interface SetStateFunction { + >(...args: [Setter]): void; + , Setter extends StateSetter>>(...args: [K1, Setter]): void; + < + K1 extends Part, + K2 extends Part>, + Setter extends StateSetter, K2>> + >( + ...args: [K1, K2, Setter] + ): void; + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + Setter extends StateSetter, K2>, K3>> + >( + ...args: [K1, K2, K3, Setter] + ): void; + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + K4 extends Part, K2>, K3>>, + Setter extends StateSetter, K2>, K3>, K4>> + >( + ...args: [K1, K2, K3, K4, Setter] + ): void; + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + K4 extends Part, K2>, K3>>, + K5 extends Part, K2>, K3>, K4>>, + Setter extends StateSetter, K2>, K3>, K4>, K5>> + >( + ...args: [K1, K2, K3, K4, K5, Setter] + ): void; + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + K4 extends Part, K2>, K3>>, + K5 extends Part, K2>, K3>, K4>>, + K6 extends Part, K2>, K3>, K4>, K5>>, + Setter extends StateSetter, K2>, K3>, K4>, K5>, K6>> + >( + ...args: [K1, K2, K3, K4, K5, K6, Setter] + ): void; + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + K4 extends Part, K2>, K3>>, + K5 extends Part, K2>, K3>, K4>>, + K6 extends Part, K2>, K3>, K4>, K5>>, + K7 extends Part, K2>, K3>, K4>, K5>, K6>>, + Setter extends StateSetter< + Next, K2>, K3>, K4>, K5>, K6>, K7> + > + >( + ...args: [K1, K2, K3, K4, K5, K6, K7, Setter] + ): void; + + // and here we give up on being accurate after 8 args + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + K4 extends Part, K2>, K3>>, + K5 extends Part, K2>, K3>, K4>>, + K6 extends Part, K2>, K3>, K4>, K5>>, + K7 extends Part, K2>, K3>, K4>, K5>, K6>>, + K8 extends Part, K2>, K3>, K4>, K5>, K6>, K7>> + >( + ...args: [K1, K2, K3, K4, K5, K6, K7, K8, ...(Part | StateSetter)[]] + ): void; +} + +export function createState(state: T | State): [State, SetStateFunction] { + function setState(...args: any[]): void { + updatePath(state, args); + } + return [state as State, setState]; +} + +type ReconcileOptions = { + key?: string | null; + merge?: boolean; +}; + +// Diff method for setState +export function reconcile( + value: T | State, + options: ReconcileOptions = {} +): (state: T extends NotWrappable ? T : State) => void { + return state => { + if (!isWrappable(state)) return value; + const targetKeys = Object.keys(value) as (keyof T)[]; + for (let i = 0, len = targetKeys.length; i < len; i++) { + const key = targetKeys[i]; + setProperty(state, key as string, value[key]); + } + const previousKeys = Object.keys(state) as (keyof T)[]; + for (let i = 0, len = previousKeys.length; i < len; i++) { + if (value[previousKeys[i]] === undefined) + setProperty(state, previousKeys[i] as string, undefined); + } + }; +} + +// Immer style mutation style +export function produce( + fn: (state: T) => void +): (state: T extends NotWrappable ? T : State) => T extends NotWrappable ? T : State { + return state => { + if (isWrappable(state)) fn(state as T); + return state; + }; +} + +export function mapArray( + list: () => T[], + mapFn: (v: T, i: () => number) => U, + options: { fallback?: () => any } = {} +): () => U[] { + const items = list(); + let s: U[] = []; + if (items.length) { + for (let i = 0, len = items.length; i < len; i++) s.push(mapFn(items[i], () => i)); + } else if (options.fallback) s = [options.fallback()]; + return () => s; +} diff --git a/packages/solid/src/static/rendering.ts b/packages/solid/src/static/rendering.ts index 28656a398..fa1d878a4 100644 --- a/packages/solid/src/static/rendering.ts +++ b/packages/solid/src/static/rendering.ts @@ -1,4 +1,4 @@ -import { State, SetStateFunction, updatePath } from "./reactive"; +import { createMemo, Owner, createContext, useContext, lookup, runWithOwner } from "./reactive"; import type { JSX } from "../jsx"; type PropsWithChildren

= P & { children?: JSX.Element }; @@ -18,22 +18,49 @@ function resolveSSRNode(node: any): string { return String(node); } +type SharedConfig = { + context?: HydrationContext; +}; +export const sharedConfig: SharedConfig = {}; + +function setHydrateContext(context?: HydrationContext): void { + sharedConfig.context = context; +} + +function nextHydrateContext(): HydrationContext | undefined { + return sharedConfig.context + ? { + ...sharedConfig.context, + id: `${sharedConfig.context.id}${sharedConfig.context.count++}.`, + count: 0 + } + : undefined; +} + export function createComponent( Comp: (props: T) => JSX.Element, props: PossiblyWrapped ): JSX.Element { + if (sharedConfig.context) { + const c = sharedConfig.context; + setHydrateContext(nextHydrateContext()); + const r = Comp(props as T); + setHydrateContext(c); + return r; + } return Comp(props as T); } -export function assignProps(target: T, source: U): T & U; -export function assignProps(target: T, source1: U, source2: V): T & U & V; -export function assignProps( - target: T, +export function mergeProps(source: T, source1: U): T & U; +export function mergeProps(source: T, source1: U, source2: V): T & U & V; +export function mergeProps( + source: T, source1: U, source2: V, source3: W ): T & U & V & W; -export function assignProps(target: any, ...sources: any): any { +export function mergeProps(...sources: any): any { + const target = {}; for (let i = 0; i < sources.length; i++) { const descriptors = Object.getOwnPropertyDescriptors(sources[i]); Object.defineProperties(target, descriptors); @@ -186,64 +213,117 @@ export interface Resource { loading: boolean; } -export function createResource( - value?: T -): [Resource, (fn: () => Promise | T) => { then: Function }] { - const resource = () => value; - resource.loading = false; - function load(fn: () => Promise | T) { - if (!globalThis._$HYDRATION.streamSSR) return { then() {} }; - resource.loading = true; - const p = fn(); - if ("then" in p) { - globalThis._$HYDRATION.register && globalThis._$HYDRATION.register(p); - return p; - } - return Promise.resolve((value = p)); - } - return [resource, load]; -} - -export interface LoadStateFunction { - ( - v: { [P in keyof T]: () => Promise | T[P] }, - reconcilerFn?: (v: Partial) => (state: State) => void - ): void; -} +type SuspenseContextType = { + resources: Map; + completed: () => void; +}; -export function createResourceState( - state: T | State +const SuspenseContext = createContext(); +export function createResource( + key: U | false | (() => U | false), + fetcher: (k: U, getPrev: () => T | undefined) => T | Promise, + value?: T ): [ - State, - LoadStateFunction, - SetStateFunction + Resource, + { + mutate: (v: T | undefined) => T | undefined; + refetch: () => void; + } ] { - (state as any).loading = {}; - function setState(...args: any[]): void { - updatePath(state, args); + const contexts = new Set(); + const id = sharedConfig.context!.id + sharedConfig.context!.count++; + let resource: { ref?: any; data?: T } = {}; + if (sharedConfig.context!.async) { + resource = sharedConfig.context!.resources[id] || (sharedConfig.context!.resources[id] = {}); + if (resource.ref) { + if (!resource.data && !resource.ref[0].loading) resource.ref[1].refetch(); + return resource.ref; + } } - function loadState( - v: { [P in keyof T]: () => Promise | T[P] }, - reconcilerFn?: (v: Partial) => (state: State) => void - ) { - const keys = Object.keys(v); - for (let i = 0; i < keys.length; i++) { - const k = keys[i] as keyof T, - [, l] = createResource(state[k]); - l(v[k]); + const read = () => { + const resolved = sharedConfig.context!.async && sharedConfig.context!.resources[id].data; + if (sharedConfig.context!.async && !resolved) { + const ctx = useContext(SuspenseContext); + if (ctx) { + ctx.resources.set(id, read); + contexts.add(ctx); + } + } + return resolved ? sharedConfig.context!.resources[id].data : value; + }; + read.loading = false; + function load() { + const ctx = sharedConfig.context!; + if (!ctx.async && !ctx.streaming) return; + if (ctx.resources && id in ctx.resources && ctx.resources[id].data) { + value = ctx.resources[id].data; + return; + } + const lookup = typeof key === "function" ? (key as () => U)() : key; + if (!lookup) return; + read.loading = true; + const p = fetcher(lookup, () => value); + if ("then" in p) { + if (ctx.writeResource) { + ctx.writeResource(id, p); + return; + } + p.then(res => { + read.loading = false; + ctx.resources[id].data = res; + notifySuspense(contexts); + return res; + }); + return; } + ctx.resources[id].data = p; } - return [state as State, loadState, setState]; + load(); + return (resource.ref = [read, { refetch: load, mutate: v => (value = v) }] as [ + Resource, + { + mutate: (v: T | undefined) => T | undefined; + refetch: () => void; + } + ]); } export function lazy(fn: () => Promise<{ default: any }>): (props: any) => string { - let p: Promise<{ default: any }>; + let resolved: (props: any) => any; + const p = fn(); + const contexts = new Set(); + p.then(mod => (resolved = mod.default)); return (props: any) => { - (p || (p = fn())).then(mod => mod.default(props)); + const id = sharedConfig.context!.id + sharedConfig.context!.count++; + if (resolved) return resolved(props); + const ctx = useContext(SuspenseContext); + const track = { loading: true }; + if (ctx) { + ctx.resources.set(id, track); + contexts.add(ctx); + } + p.then(() => { + track.loading = false; + notifySuspense(contexts); + }); return ""; }; } +function suspenseComplete(c: SuspenseContextType) { + for (let r of c.resources.values()) { + if (r.loading) return false; + } + return true; +} + +function notifySuspense(contexts: Set) { + for (const c of contexts) { + if (suspenseComplete(c)) c.completed(); + } + contexts.clear(); +} + export function useTransition(): [() => boolean, (fn: () => any) => void] { return [ () => false, @@ -256,22 +336,13 @@ export function useTransition(): [() => boolean, (fn: () => any) => void] { type HydrationContext = { id: string; count: number; - registry?: Map; -}; - -type GlobalHydration = { - context?: HydrationContext; - register?: (v: Promise) => void; - loadResource?: () => Promise; - resources?: { [key: string]: any }; - asyncSSR?: boolean; - streamSSR?: boolean; + writeResource?: (id: string, v: Promise) => void; + resources: Record; + suspense: Record; + async?: boolean; + streaming?: boolean; }; -declare global { - var _$HYDRATION: GlobalHydration; -} - export function SuspenseList(props: { children: string; revealOrder: "forwards" | "backwards" | "together"; @@ -281,8 +352,73 @@ export function SuspenseList(props: { return props.children; } +const SUSPENSE_GLOBAL = Symbol("suspense-global"); export function Suspense(props: { fallback: string; children: string }) { + const ctx = sharedConfig.context!; // TODO: look at not always going to fallback - props.children; - return props.fallback; + if (ctx.streaming) createComponent(() => props.children, {}); + if (!ctx.async) return props.fallback; + + const id = ctx.id + ctx.count; + const done = ctx.async ? lookup(Owner, SUSPENSE_GLOBAL)(id) : () => {}; + const o = Owner!; + const value: SuspenseContextType = + ctx.suspense[id] || + (ctx.suspense[id] = { + resources: new Map(), + completed: () => { + const res = runSuspense(); + if (suspenseComplete(value)) { + done(resolveSSRNode(res)); + } + } + }); + function runSuspense() { + setHydrateContext({ ...ctx, count: 0 }); + return runWithOwner(o, () => { + return createComponent(SuspenseContext.Provider, { + value, + get children() { + return props.children; + } + }); + }); + } + const res = runSuspense(); + if (suspenseComplete(value)) { + done(); + return res; + } + return sharedConfig.context!.async ? { t: `<#${id}#>` } : props.fallback; +} + +const SUSPENSE_REPLACE = /<#([0-9\.]+)\#>/; +export function awaitSuspense(fn: () => any) { + return new Promise(resolve => { + const registry = new Set(); + const cache: Record = Object.create(null); + const res = createMemo(() => { + Owner!.context = { [SUSPENSE_GLOBAL]: getCallback }; + return fn(); + }); + if (!registry.size) resolve(res()); + function getCallback(key: string) { + registry.add(key); + return (value: string) => { + if (value) cache[key] = value; + registry.delete(key); + if (!registry.size) + queueMicrotask(() => { + let source = resolveSSRNode(res()); + let final = ""; + let match: any; + while ((match = source.match(SUSPENSE_REPLACE))) { + final += source.substring(0, match.index); + source = cache[match[1]] + source.substring(match.index + match[0].length); + } + resolve(final + source); + }); + }; + } + }); } diff --git a/packages/solid/test/component.spec.ts b/packages/solid/test/component.spec.ts index 6d5169b21..2f5e4abed 100644 --- a/packages/solid/test/component.spec.ts +++ b/packages/solid/test/component.spec.ts @@ -1,4 +1,4 @@ -import { createComponent, assignProps, splitProps, createState } from "../src"; +import { createComponent, mergeProps, splitProps, createState } from "../src"; type SimplePropTypes = { a?: string | null; @@ -31,7 +31,7 @@ describe("Set Default Props", () => { c: "j" }, defaults: SimplePropTypes = { a: "yy", b: "ggg", d: "DD" }; - props = assignProps(defaults, props); + props = mergeProps(defaults, props); expect(props.a).toBe("ji"); expect(props.b).toBe(null); expect(props.c).toBe("j"); @@ -50,7 +50,7 @@ describe("Clone Props", () => { b: null, c: "j" }; - const newProps = assignProps({}, props); + const newProps = mergeProps({}, props); expect(reactive).toBe(false); expect(newProps.a).toBe("ji"); expect(reactive).toBe(true); @@ -62,13 +62,13 @@ describe("Clone Props", () => { describe("Clone State", () => { const [state, setState] = createState<{a: string, b: string, c?: string}>({ a: "Hi", b: "Jo" }); - const clone = assignProps({}, state); + const clone = mergeProps(state); expect(clone.a).toBe("Hi"); expect(clone.b).toBe("Jo"); setState({ a: "Greetings", c: "John" }); expect(clone.a).toBe("Greetings"); expect(clone.b).toBe("Jo"); - expect(clone.c).toBe(undefined); + expect(clone.c).toBe("John"); }) describe("SplitProps Props", () => { diff --git a/packages/solid/test/dev.spec.ts b/packages/solid/test/dev.spec.ts index c14c7619f..f26525aa3 100644 --- a/packages/solid/test/dev.spec.ts +++ b/packages/solid/test/dev.spec.ts @@ -1,6 +1,6 @@ import { createRoot, - getContextOwner, + getOwner, createSignal, createState, createEffect, @@ -9,14 +9,14 @@ import { describe("Dev features", () => { test("Reactive graph serialization", () => { - let owner: ReturnType, set1: (v: number) => number, setState1: any; + let owner: ReturnType, set1: (v: number) => number, setState1: any; const SNAPSHOTS = [ `{"s1773325850":5,"s1773325850-1":5,"s533736025":{"firstName":"John","lastName":"Smith"},"c-1":{"explicit":6}}`, `{"s1773325850":7,"s1773325850-1":5,"s533736025":{"firstName":"Matt","lastName":"Smith","middleInitial":"R."},"c-1":{"explicit":6}}` ]; createRoot(() => { - owner = getContextOwner(); + owner = getOwner(); const [s, set] = createSignal(5); const [s2] = createSignal(5); createEffect(() => { diff --git a/packages/solid/test/resource.spec.ts b/packages/solid/test/resource.spec.ts index e6a098072..6c1563d6d 100644 --- a/packages/solid/test/resource.spec.ts +++ b/packages/solid/test/resource.spec.ts @@ -2,39 +2,34 @@ import { createRoot, createSignal, createResource, - createResourceState, createComputed, + createState, createRenderEffect, onError, - SetStateFunction, Resource, - State, - reconcile + reconcile, + State } from "../src"; describe("Simulate a dynamic fetch", () => { let resolve: (v: string) => void, reject: (r: string) => void, - trigger: (v: number) => void, - load: (v: () => Promise) => void, - i: number, + trigger: (v: string) => void, value: Resource, error: string; - function fetcher(id: number) { - return () => - new Promise((r, f) => { - resolve = r; - reject = f; - }); + function fetcher(id: string) { + return new Promise((r, f) => { + resolve = r; + reject = f; + }); } test("initial async resource", async done => { createRoot(() => { - const [id, setId] = createSignal(1); - [value, load] = createResource(); + const [id, setId] = createSignal("1"); trigger = setId; onError(e => (error = e)); - createComputed(() => (i = id()) && load(fetcher(i))); + [value] = createResource(id, fetcher); createRenderEffect(value); }); expect(value()).toBeUndefined(); @@ -47,10 +42,10 @@ describe("Simulate a dynamic fetch", () => { }); test("test out of order", async done => { - trigger(2); + trigger("2"); expect(value.loading).toBe(true); const resolve1 = resolve; - trigger(3); + trigger("3"); const resolve2 = resolve; resolve2("Jake"); resolve1("Jo"); @@ -61,7 +56,7 @@ describe("Simulate a dynamic fetch", () => { }); test("promise rejection", async done => { - trigger(4); + trigger("4"); expect(value.loading).toBe(true); reject("Because I said so"); await Promise.resolve(); @@ -71,103 +66,6 @@ describe("Simulate a dynamic fetch", () => { }); }); -describe("Simulate a dynamic fetch with state", () => { - let resolve: (v: string) => void, - reject: (r: string) => void, - trigger: (v: number) => void, - load: ( - v: { [k: number]: () => Promise | string }, - r?: (v: any) => (state: any) => void - ) => void, - setUsers: SetStateFunction<{ [id: number]: string }>, - users: State<{ [id: number]: string; loading: { [id: number]: boolean } }>, - count = 0; - function fetcher(): Promise { - return new Promise((r, f) => { - resolve = r; - reject = f; - }); - } - - test("initial async resource", async done => { - createRoot(() => { - const [id, setId] = createSignal(1); - [users, load, setUsers] = createResourceState<{ [id: number]: string }>({ 6: "Rio" }); - trigger = setId; - createComputed(() => load({ [id()]: fetcher })); - createComputed(() => (users[5], count++)); - }); - expect(users[1]).toBeUndefined(); - expect(users.loading[1]).toBe(true); - resolve("John"); - await Promise.resolve(); - await Promise.resolve(); - expect(users[1]).toBe("John"); - expect(users.loading[1]).toBe(false); - done(); - }); - - test("test multiple loads", async done => { - trigger(2); - expect(users.loading[2]).toBe(true); - const resolve1 = resolve; - trigger(3); - const resolve2 = resolve; - resolve2("Jake"); - resolve1("Jo"); - await Promise.resolve(); - await Promise.resolve(); - expect(users[3]).toBe("Jake"); - expect(users.loading[3]).toBe(false); - done(); - }); - - test("promise rejection", async done => { - trigger(4); - expect(users.loading[4]).toBe(true); - reject("Because I said so"); - await Promise.resolve(); - await Promise.resolve(); - expect(users.loading[4]).toBe(false); - done(); - }); - - test("setState", () => { - setUsers(5, "Jordy"); - expect(users[5]).toBe("Jordy"); - expect(count).toBe(2); - }); - - test("test loading same value", () => { - load({ 5: () => "Jordy" }); - expect(users[5]).toBe("Jordy"); - expect(count).toBe(2); - }); - - test("custom reconciler", async done => { - const reconcile = (v: string) => (state: string) => `${state} ${v}`; - load({ 6: () => new Promise(r => r("Jerry")) }, reconcile); - await Promise.resolve(); - await Promise.resolve(); - expect(users[6]).toBe("Rio Jerry"); - done(); - }); - - test("setState tracked", () => { - createRoot(() => { - let runs = 0; - createComputed(() => { - users[7]; - runs++; - }); - expect(runs).toBe(1); - setUsers({ 7: "Jimbo" }); - expect(users[7]).toBe("Jimbo"); - expect(runs).toBe(2); - }); - }); -}); - describe("Simulate a dynamic fetch with state and reconcile", () => { interface User { firstName: string; @@ -177,45 +75,50 @@ describe("Simulate a dynamic fetch with state and reconcile", () => { }; } let resolve: (v: User) => void, - load: ( - v: { [k: number]: () => Promise | User }, - r?: (v: any) => (state: any) => void - ) => void, - users: State<{ [id: number]: User; loading: { [id: number]: boolean } }>, + refetch: () => void, + user: Resource, + state: { user?: User; userLoading: boolean }, count = 0; - function fetcher() { + function fetcher(_: string, getPrev: () => User | undefined) { return new Promise(r => { resolve = r; - }); + }).then(value => reconcile(value)(getPrev() as State)); } const data: User[] = []; - data.push({ firstName: "John", address: { streetNumber: 4, streetName: "Grindel Rd" } }) - data.push({ ...data[0], firstName: "Joseph" }) + data.push({ firstName: "John", address: { streetNumber: 4, streetName: "Grindel Rd" } }); + data.push({ ...data[0], firstName: "Joseph" }); test("initial async resource", async done => { createRoot(() => { - [users, load] = createResourceState<{ [id: number]: User }>({}); - createComputed(() => (users[0], count++)); + [user, { refetch }] = createResource("user", fetcher); + [state] = createState<{ user?: User; userLoading: boolean }>({ + get user() { + return user(); + }, + get userLoading() { + return user.loading; + } + }); + createComputed(() => (state.user, count++)); }); - load({ 0: fetcher }); - expect(users[0]).toBeUndefined(); - expect(users.loading[0]).toBe(true); + expect(state.user).toBeUndefined(); + expect(state.userLoading).toBe(true); resolve(data[0]); await Promise.resolve(); await Promise.resolve(); - expect(users[0]).toStrictEqual(data[0]); - expect(users.loading[0]).toBe(false); + expect(state.user).toStrictEqual(data[0]); + expect(state.userLoading).toBe(false); expect(count).toBe(2); - load({ 0: fetcher }, reconcile); - expect(users.loading[0]).toBe(true); + refetch(); + expect(state.userLoading).toBe(true); resolve(data[1]); await Promise.resolve(); await Promise.resolve(); - expect(users[0]).toStrictEqual(data[0]); - expect(users[0].firstName).toBe("Joseph"); - expect(users[0].address).toStrictEqual(data[0].address); - expect(users.loading[0]).toBe(false); + expect(state.user).toStrictEqual(data[0]); + expect(state.user!.firstName).toBe("Joseph"); + expect(state.user!.address).toStrictEqual(data[0].address); + expect(state.userLoading).toBe(false); expect(count).toBe(2); done(); }); @@ -224,9 +127,8 @@ describe("Simulate a dynamic fetch with state and reconcile", () => { describe("using Resource with no root", () => { test("loads default value", () => { expect(() => { - const [, load] = createResource(); let resolve: (v: string) => void; - load(() => new Promise(r => (resolve = r))); + createResource("error", () => new Promise(r => (resolve = r))); resolve!("Hi"); }).not.toThrow(); }); diff --git a/packages/solid/web/server-async/asyncSSR.js b/packages/solid/web/server-async/asyncSSR.js deleted file mode 100644 index 40ed8600a..000000000 --- a/packages/solid/web/server-async/asyncSSR.js +++ /dev/null @@ -1 +0,0 @@ -export * from "dom-expressions/src/asyncSSR"; \ No newline at end of file diff --git a/packages/solid/web/server-async/index.ts b/packages/solid/web/server-async/index.ts deleted file mode 100644 index 521670a36..000000000 --- a/packages/solid/web/server-async/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ssr, ssrSpread } from "./asyncSSR"; -import { createMemo, untrack, splitProps, Component, JSX } from "solid-js"; - -export * from "./asyncSSR"; - -export { - For, - Show, - Suspense, - SuspenseList, - Switch, - Match, - Index, - ErrorBoundary, - assignProps -} from "solid-js"; - -export const isServer = true; - -export function spread() {} - -export function Dynamic( - props: T & { children?: any; component?: Component | string | keyof JSX.IntrinsicElements } -): () => JSX.Element { - const [p, others] = splitProps(props, ["component"]); - return createMemo(() => { - const comp = p.component, - t = typeof comp; - - if (comp) { - if (t === "function") return untrack(() => (comp as Function)(others as any)); - else if (t === "string") { - const [local, sOthers] = splitProps(others, ["children"]); - return ssr([`<${comp} `, ">", ``], ssrSpread(sOthers), local.children || ""); - } - } - }); -} - -export function Portal(props: { mount?: Node; useShadow?: boolean; children: JSX.Element }) { - return ""; -} diff --git a/packages/solid/web/server/index.ts b/packages/solid/web/server/index.ts index 77971f571..195034616 100644 --- a/packages/solid/web/server/index.ts +++ b/packages/solid/web/server/index.ts @@ -1,7 +1,7 @@ -import { ssr, ssrSpread } from "./syncSSR"; +import { ssr, ssrSpread } from "./server"; import { splitProps, Component, JSX } from "solid-js"; -export * from "./syncSSR"; +export * from "./server"; export { For, @@ -12,7 +12,7 @@ export { Match, Index, ErrorBoundary, - assignProps + mergeProps } from "solid-js"; export const isServer = true; diff --git a/packages/solid/web/server/server.js b/packages/solid/web/server/server.js new file mode 100644 index 000000000..4ccb67de8 --- /dev/null +++ b/packages/solid/web/server/server.js @@ -0,0 +1 @@ +export * from "dom-expressions/src/server"; \ No newline at end of file diff --git a/packages/solid/web/server/syncSSR.js b/packages/solid/web/server/syncSSR.js deleted file mode 100644 index 85fa81f3d..000000000 --- a/packages/solid/web/server/syncSSR.js +++ /dev/null @@ -1 +0,0 @@ -export * from "dom-expressions/src/syncSSR"; \ No newline at end of file diff --git a/packages/solid/web/src/client.js b/packages/solid/web/src/client.js new file mode 100644 index 000000000..0260ac464 --- /dev/null +++ b/packages/solid/web/src/client.js @@ -0,0 +1 @@ +export * from "dom-expressions/src/client"; \ No newline at end of file diff --git a/packages/solid/web/src/core.ts b/packages/solid/web/src/core.ts index 736e449dc..e55204975 100644 --- a/packages/solid/web/src/core.ts +++ b/packages/solid/web/src/core.ts @@ -4,10 +4,12 @@ import { createRenderEffect, createMemo, createComponent, - getContextOwner + getOwner, + sharedConfig, + awaitSuspense } from "solid-js"; // reactive injection for dom-expressions function memo(fn: () => any, equal: boolean) { return createMemo(fn, undefined, equal); } -export { getContextOwner as currentContext, createComponent, createRoot as root, createRenderEffect as effect, memo } +export { getOwner, createComponent, createRoot as root, createRenderEffect as effect, memo, sharedConfig, awaitSuspense as asyncWrap } diff --git a/packages/solid/web/src/index.ts b/packages/solid/web/src/index.ts index 7695c6b8d..8bc690acb 100644 --- a/packages/solid/web/src/index.ts +++ b/packages/solid/web/src/index.ts @@ -1,105 +1,105 @@ -import { insert, spread, SVGElements } from "./runtime"; -import { - createSignal, - createMemo, - onCleanup, - untrack, - splitProps, - Component, - JSX, - createRoot -} from "solid-js"; - -export * from "./runtime"; - -export { - For, - Show, - Suspense, - SuspenseList, - Switch, - Match, - Index, - ErrorBoundary, - assignProps -} from "solid-js"; - -export * from "./server-mock"; -export const isServer = false; -const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; - -function createElement(tagName: string, isSVG = false): HTMLElement|SVGElement { - return isSVG ? document.createElementNS(SVG_NAMESPACE, tagName) : - document.createElement(tagName); -} - -export function Portal(props: { - mount?: Node; - useShadow?: boolean; - isSVG?: boolean; - children: JSX.Element; -}) { - const hydration = globalThis._$HYDRATION; - const { useShadow } = props, - marker = document.createTextNode(""), - mount = props.mount || document.body; - - // don't render when hydrating - function renderPortal() { - if (hydration && hydration.context) { - const [s, set] = createSignal(false); - queueMicrotask(() => set(true)); - return () => s() && props.children; - } else return () => props.children; - } - - if (mount instanceof HTMLHeadElement) { - const [clean, setClean] = createSignal(false); - const cleanup = () => setClean(true); - createRoot(dispose => insert(mount, () => (!clean() ? renderPortal()() : dispose()), null)); - onCleanup(() => { - if (hydration && hydration.context) queueMicrotask(cleanup); - else cleanup(); - }); - } else { - const container = createElement(props.isSVG ? "g" : "div", props.isSVG), - renderRoot = - useShadow && container.attachShadow ? container.attachShadow({ mode: "open" }) : container; - - Object.defineProperty(container, "host", { - get() { - return marker.parentNode; - } - }); - insert(renderRoot, renderPortal()); - mount.appendChild(container); - (props as any).ref && (props as any).ref(container); - onCleanup(() => mount.removeChild(container)); - } - return marker; -} - -type DynamicProps = T&{ - children?: any; - component?: Component|string|keyof JSX.IntrinsicElements; -}; - -export function Dynamic(props: DynamicProps): () => JSX.Element { - const [p, others] = splitProps(props, ["component"]); - return createMemo(() => { - const component = p.component as Function|string; - switch (typeof component) { - case "function": - return untrack(() => component(others)); - - case "string": - const isSvg = SVGElements.has(component); - const el = createElement(component, isSvg); - spread(el, others, isSvg); - return el; - - default: - break; - } - }); -} +import { insert, spread, SVGElements } from "./client"; +import { + createSignal, + createMemo, + onCleanup, + untrack, + splitProps, + Component, + JSX, + createRoot, + sharedConfig +} from "solid-js"; + +export * from "./client"; + +export { + For, + Show, + Suspense, + SuspenseList, + Switch, + Match, + Index, + ErrorBoundary, + mergeProps +} from "solid-js"; + +export * from "./server-mock"; +export const isServer = false; +const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; + +function createElement(tagName: string, isSVG = false): HTMLElement|SVGElement { + return isSVG ? document.createElementNS(SVG_NAMESPACE, tagName) : + document.createElement(tagName); +} + +export function Portal(props: { + mount?: Node; + useShadow?: boolean; + isSVG?: boolean; + children: JSX.Element; +}) { + const { useShadow } = props, + marker = document.createTextNode(""), + mount = props.mount || document.body; + + // don't render when hydrating + function renderPortal() { + if (sharedConfig.context) { + const [s, set] = createSignal(false); + queueMicrotask(() => set(true)); + return () => s() && props.children; + } else return () => props.children; + } + + if (mount instanceof HTMLHeadElement) { + const [clean, setClean] = createSignal(false); + const cleanup = () => setClean(true); + createRoot(dispose => insert(mount, () => (!clean() ? renderPortal()() : dispose()), null)); + onCleanup(() => { + if (sharedConfig.context) queueMicrotask(cleanup); + else cleanup(); + }); + } else { + const container = createElement(props.isSVG ? "g" : "div", props.isSVG), + renderRoot = + useShadow && container.attachShadow ? container.attachShadow({ mode: "open" }) : container; + + Object.defineProperty(container, "host", { + get() { + return marker.parentNode; + } + }); + insert(renderRoot, renderPortal()); + mount.appendChild(container); + (props as any).ref && (props as any).ref(container); + onCleanup(() => mount.removeChild(container)); + } + return marker; +} + +type DynamicProps = T&{ + children?: any; + component?: Component|string|keyof JSX.IntrinsicElements; +}; + +export function Dynamic(props: DynamicProps): () => JSX.Element { + const [p, others] = splitProps(props, ["component"]); + return createMemo(() => { + const component = p.component as Function|string; + switch (typeof component) { + case "function": + return untrack(() => component(others)); + + case "string": + const isSvg = SVGElements.has(component); + const el = createElement(component, isSvg); + spread(el, others, isSvg); + return el; + + default: + break; + } + }); +} diff --git a/packages/solid/web/src/runtime.js b/packages/solid/web/src/runtime.js deleted file mode 100644 index 65e55b7d8..000000000 --- a/packages/solid/web/src/runtime.js +++ /dev/null @@ -1 +0,0 @@ -export * from "dom-expressions/src/runtime"; \ No newline at end of file diff --git a/packages/solid/web/src/server-mock.ts b/packages/solid/web/src/server-mock.ts index 3b51dde13..b07f36bb6 100644 --- a/packages/solid/web/src/server-mock.ts +++ b/packages/solid/web/src/server-mock.ts @@ -1,12 +1,39 @@ //@ts-nocheck +type RenderToStringResults = { + html: string; + script: string +} + +type RenderToStreamResults = { + stream: T; + script: string; +} + export function renderToString( fn: () => T, options?: { + eventNames?: string[]; + } +): RenderToStringResults; +export function renderToStringAsync( + fn: () => T, + options?: { + eventNames?: string[]; timeoutMs?: number; } -): T extends Promise ? Promise : string {} -export function renderToNodeStream(fn: () => T): NodeJS.ReadableStream {} -export function renderToWebStream(fn: () => T): ReadableStream {} +): Promise; +export function renderToNodeStream( + fn: () => T, + options?: { + eventNames?: string[]; + } +): RenderToStreamResults; +export function renderToWebStream( + fn: () => T, + options?: { + eventNames?: string[]; + } +): RenderToStreamResults; export function ssr(template: string[] | string, ...nodes: any[]): { t: string } {} export function resolveSSRNode(node: any): string {} export function ssrClassList(value: { [k: string]: boolean }): string {} diff --git a/packages/solid/web/test/dynamic.spec.tsx b/packages/solid/web/test/dynamic.spec.tsx index 4256c4824..24d77a430 100644 --- a/packages/solid/web/test/dynamic.spec.tsx +++ b/packages/solid/web/test/dynamic.spec.tsx @@ -1,5 +1,5 @@ /* @jsxImportSource solid-js */ -import { createRoot, createSignal, Component, JSX } from "../../src"; +import { createRoot, createSignal, createState, Component, JSX } from "../../src"; import { Dynamic } from "../src"; describe("Testing Dynamic control flow", () => { @@ -49,3 +49,47 @@ describe("Testing Dynamic control flow", () => { expect(div.querySelector('path')).toBeInstanceOf(SVGElement); }); }); + + +describe("Testing Dynamic with state spread", () => { + let div: HTMLDivElement, disposer: () => void; + + interface DynamicProps { + title: string; + } + const [comp, setComp] = createSignal | keyof JSX.IntrinsicElements>(), + [state, setState] = createState({ + title: "Smith" + }); + const Component = () => ( +

+ +
+ ), + CompA: Component = props =>
Hi {props.title}
, + CompB: Component = props => Yo {props.title}; + + beforeEach(() => { + createRoot(dispose => { + disposer = dispose; + ; + }); + }) + + afterEach(() => disposer()); + + test("Toggle Dynamic control flow", () => { + expect(div.innerHTML).toBe(""); + setComp(CompA); + expect(div.innerHTML).toBe("
Hi Smith
"); + setState("title", "Smithers"); + expect(div.innerHTML).toBe("
Hi Smithers
"); + setComp(CompB); + expect(div.innerHTML).toBe("Yo Smithers"); + setComp("h1"); + expect(div.innerHTML).toBe(`

`); + setState("title", "Sunny") + expect(div.innerHTML).toBe(`

`); + expect(div.querySelector('h1')).toBeInstanceOf(HTMLElement); + }); +}); diff --git a/packages/solid/web/test/element.spec.tsx b/packages/solid/web/test/element.spec.tsx index 9ec4d0089..4fd663d6e 100644 --- a/packages/solid/web/test/element.spec.tsx +++ b/packages/solid/web/test/element.spec.tsx @@ -13,11 +13,11 @@ describe("Basic element attributes", () => { }, onClick: () => console.log("clicked") }, - d = createRoot(() =>
) as HTMLDivElement & { __click: any }; + d = createRoot(() =>
) as HTMLDivElement & { $$click: any }; expect(div!).toBe(d); expect(d.id).toBe("main"); expect(d.title).toBe("main"); - expect(d.__click).toBeDefined(); + expect(d.$$click).toBeDefined(); expect(d.innerHTML).toBe("

Hi

"); }); diff --git a/packages/solid/web/test/suspense.spec.tsx b/packages/solid/web/test/suspense.spec.tsx index daff62154..42d4f4098 100644 --- a/packages/solid/web/test/suspense.spec.tsx +++ b/packages/solid/web/test/suspense.spec.tsx @@ -1,12 +1,5 @@ /* @jsxImportSource solid-js */ -import { - lazy, - createSignal, - createComputed, - createResource, - createResourceState, - useTransition -} from "../../src"; +import { lazy, createSignal, createResource, useTransition } from "../../src"; import { render, Suspense, SuspenseList } from "../src"; describe("Testing Suspense", () => { @@ -16,9 +9,10 @@ describe("Testing Suspense", () => { [triggered, trigger] = createSignal(false); const LazyComponent = lazy(() => new Promise(r => resolvers.push(r))), ChildComponent = (props: { greeting: string }) => { - let [value, load] = createResource(""); - createComputed( - () => triggered() && load(() => new Promise(r => setTimeout(() => r("Jo"), 300))) + let [value] = createResource( + () => triggered() ? "child" : null, + () => new Promise(r => setTimeout(() => r("Jo"), 300)), + "" ); return () => `${props.greeting} ${value()}`; }, @@ -64,39 +58,9 @@ describe("Testing Suspense", () => { }); }); -describe("Testing Suspense with State", () => { - let div = document.createElement("div"), - disposer: () => void; - const ChildComponent = (props: { name: string }) => { - const [state, load] = createResourceState({ greeting: "" }); - load({ greeting: () => new Promise(r => setTimeout(() => r("Hey"), 300)) }); - return <>{`${state.greeting}, ${props.name}`}; - }, - Component = () => ( - - - - - ); - - test("Create Suspense control flow", done => { - disposer = render(Component, div); - expect(div.innerHTML).toBe("Loading"); - setTimeout(() => { - expect(div.innerHTML).toBe("Hey, Jo!Hey, Jacob!"); - done(); - }, 400); - }); - - test("dispose", () => { - div.innerHTML = ""; - disposer(); - }); -}); - describe("SuspenseList", () => { - const promiseFactory = (time: number, v: string) => { - return () => + const promiseFactory = (time: number) => { + return (v: string) => new Promise(r => { setTimeout(() => { r(v); @@ -104,18 +68,15 @@ describe("SuspenseList", () => { }); }, A = () => { - const [value, load] = createResource(); - load(promiseFactory(200, "A")); + const [value] = createResource("A", promiseFactory(200)); return
{value()}
; }, B = () => { - const [value, load] = createResource(); - load(promiseFactory(100, "B")); + const [value] = createResource("B", promiseFactory(100)); return
{value()}
; }, C = () => { - const [value, load] = createResource(); - load(promiseFactory(300, "C")); + const [value] = createResource("C", promiseFactory(300)); return
{value()}
; }; diff --git a/packages/solid/web/test/switch.spec.tsx b/packages/solid/web/test/switch.spec.tsx index 4c909e843..877d528c1 100644 --- a/packages/solid/web/test/switch.spec.tsx +++ b/packages/solid/web/test/switch.spec.tsx @@ -1,6 +1,6 @@ /* @jsxImportSource solid-js */ -import { render, Switch, Match } from "../src"; -import { createRoot, createSignal } from "../../src"; +import { render, Switch, Match, For } from "../src"; +import { createRoot, createSignal, createState } from "../../src"; describe("Testing a single match switch control flow", () => { let div: HTMLDivElement, disposer: () => void; @@ -72,7 +72,7 @@ describe("Testing an only child Switch control flow", () => { setCount(4); expect(div.innerHTML).toBe("2"); expect(div.firstChild).toBe(c); - }) + }); test("dispose", () => disposer()); }); @@ -115,6 +115,45 @@ describe("Testing function handler Switch control flow", () => { test("dispose", () => disposer()); }); +describe("Testing a For in a Switch control flow", () => { + let div: HTMLDivElement, disposer: () => void; + const [state, setState] = createState({ + users: [ + { firstName: "Jerry", certified: false }, + { firstName: "Janice", certified: false } + ] + }); + const Component = () => ( +
+ + + {user => {user.firstName}} + + +
+ ); + + test("Create Switch control flow", () => { + createRoot(dispose => { + disposer = dispose; + ; + }); + + expect(div.innerHTML).toBe("fallback"); + }); + + test("Toggle Switch control flow", () => { + setState("users", 1, "certified", true); + expect(div.innerHTML).toBe("Janice"); + setState("users", 0, "certified", true); + expect(div.innerHTML).toBe("Jerry"); + setState("users", u => [{ firstName: "Gordy", certified: true }, ...u]); + expect(div.innerHTML).toBe("Gordy"); + }); + + test("dispose", () => disposer()); +}); + describe("Test top level switch control flow", () => { let div = document.createElement("div"), disposer: () => void; @@ -125,7 +164,7 @@ describe("Test top level switch control flow", () => { ); - test("Create when control flow", () => { + test("Create switch control flow", () => { disposer = render(Component, div); expect(div.innerHTML).toBe("fallback"); diff --git a/tsconfig.test.json b/tsconfig.test.json index 0ed2f882f..d465441cb 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -4,7 +4,7 @@ "target": "esnext", "moduleResolution": "node", "strict": true, - "lib": ["dom", "es2015", "dom.iterable"], + "lib": ["dom", "esnext", "dom.iterable"], "jsx": "preserve" }, "exclude": ["node_modules"]