From d812a67fb03b84cdfc5f4558cd13bdb160892131 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sat, 15 Aug 2026 09:24:41 -0700 Subject: [PATCH] feat: Solid 2 --- README.md | 45 ++++++++++++++-- index.d.ts | 35 +++++++++---- nativescript.webpack.js | 57 +++++++++++--------- package.json | 7 +-- src/renderer.js | 113 +++++++++++++++++++++++----------------- 5 files changed, 164 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 503a7f3..44d723d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # NativeScript SolidJS -### **Custom render and patches for SolidJS to work with [DOMiNATIVE](https://github.com/SudoMaker/DOMiNATIVE) on [NativeScript](https://nativescript.org/)** - -[Playground](https://stackblitz.com/edit/nativescript-dominative-solid?file=app/app.jsx) +[Try on StackBlitz](https://stackblitz.com/edit/nativescript-dominative-solid?file=app/app.jsx) --- @@ -11,10 +9,33 @@ Via npm: ```shell -npm install @nativescript-community/solid-js dominative undom-ng solid-js @babel/preset-typescript babel-preset-solid +npm install @nativescript-community/solid-js dominative undom-ng solid-js @solidjs/universal @babel/preset-typescript babel-preset-solid +``` + +**Note:** `dominative`, `undom-ng`, `solid-js`, `@solidjs/universal` are peer dependencies, you have to install them manually. As the benefit for using peer dependencies, you'll be able to upgrade these dependencies directly from upstream, no need to wait for an update with `@nativescript-community/solid-js` + +| `@nativescript-community/solid-js` | `solid-js` | +| ---------------------------------- | ----------- | +| `2.x` | `2.x` | +| `0.1.x` | `1.8`–`1.9` | + +--- + +## Build setup + +The JSX compiler has to emit calls against this renderer rather than the DOM one, so whichever bundler you use needs these compiler options: + +```js +{ + generate: 'universal', + hydratable: false, + moduleName: '@nativescript-community/solid-js', +} ``` -**Note:** `dominative`, `undom-ng`, `solid-js` are peer dependencies, you have to install them manually. As the benefit for using peer dependencies, you'll be able to upgrade these dependencies directly from upstream, no need to wait for an update with `@nativescript-community/solid-js` +With webpack, `nativescript.webpack.js` in this package wires that up (plus the `@NativeClass` transform) — register it from your project's `webpack.config.js`. With Vite, pass them to `@solidjs/vite-plugin` as its `solid` option. + +Either way, pin `solid-js`, `@solidjs/signals`, and `@solidjs/universal` to their browser bundles. Solid 2's exports map resolves the `node` condition to the SSR build, which renders once and never updates. --- @@ -52,6 +73,20 @@ Application.run({ create }) --- +## Upgrading from Solid 1.x + +Solid 2 is a rewrite; [its cheatsheet](https://github.com/solidjs/solid/blob/main/packages/solid/CHEATSHEET.md) and [migration guide](https://github.com/solidjs/solid/blob/main/documentation/solid-2.0/MIGRATION.md) cover the reactive changes (two-argument `createEffect`, draft-first store setters, deferred reads until flush). What changes for a NativeScript app specifically: + +- `solid-js/store` merged into `solid-js`, and `solid-js/universal` moved to `@solidjs/universal` — install it alongside `solid-js`. +- Control flow re-exported from this package is now `For`, `Repeat`, `Show`, `Switch`, `Match`, `Loading`, `Reveal`, `Errored`. `Suspense` → `Loading`, `SuspenseList` → `Reveal`, `ErrorBoundary` → `Errored`, and `` → ``. +- `onMount` is gone; use `onSettled`, which takes the teardown as its return value. +- `use:` directives are gone (this package no longer exports `use`). A directive is now a ref: `ref={myDirective(options)}`, and refs compose as arrays. +- `` callback arguments follow the keying mode. The default is keyed by identity, giving `(item, indexAccessor)`; `keyed={false}` gives `(itemAccessor, index)`. +- Event handlers keep the `on:` / `oncapture:` prefixes described below. Solid 2's DOM renderer dropped them, but NativeScript event names are case sensitive and don't bubble, so they stay the binding for this renderer. +- `solid-refresh` has no Solid 2 release that matches this range, so the webpack config no longer registers it. NativeScript's own HMR still replaces modules. + +--- + ## Caveats ### Event handling diff --git a/index.d.ts b/index.d.ts index c554e15..a0793cd 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,8 +3,11 @@ export type AnyNode = any; /** Mount a Solid tree into a NativeScript host element. Returns a disposer. */ export function render(code: () => any, element: AnyNode): () => void; -/** Create a reactive effect. */ -export function effect(fn: (prev?: T) => T, init?: T): void; +/** Create a reactive effect. The compute phase tracks; the apply phase does not. */ +export function effect( + fn: (prev?: T) => T, + effect: (value: T, prev?: T) => void +): void; /** Create a memoized computation. */ export function memo(fn: () => T, equal?: boolean): () => T; @@ -12,8 +15,11 @@ export function memo(fn: () => T, equal?: boolean): () => T; /** Create a component instance. */ export function createComponent

(Comp: (props: P) => any, props: P): any; -/** Create an element node by tag name. */ -export function createElement(tagName: string): AnyNode; +/** Create an element node by tag name, applying any compile-time static props. */ +export function createElement( + tagName: string, + staticProps?: Record +): AnyNode; /** Create a text node. */ export function createTextNode(value: string): AnyNode; @@ -33,20 +39,27 @@ export function insert( export function spread( node: AnyNode, props: Record, - isSVG?: boolean, skipChildren?: boolean ): AnyNode; /** Set a single prop on a node. */ export function setProp(node: AnyNode, name: string, value: any, prev?: any): void; -/** Merge props objects similarly to Solid's mergeProps. */ +/** Merge props objects similarly to Solid's merge. */ export function mergeProps( ...sources: Array | Partial | undefined> ): T & U; -/** Optional helper to call a function if provided, returning its result. */ -export function use( - fn: ((args: TArgs) => TReturn) | undefined, - args: TArgs -): TReturn | undefined; +/** Apply a ref (or array of refs) to a node immediately. */ +export function applyRef( + r: ((element: AnyNode) => void) | ((element: AnyNode) => void)[], + element: AnyNode +): void; + +/** Resolve a ref accessor untracked and apply it to a node. */ +export function ref( + fn: () => ((element: AnyNode) => void) | ((element: AnyNode) => void)[], + element: AnyNode +): void; + +export { For, Repeat, Show, Switch, Match, Loading, Reveal, Errored } from 'solid-js'; diff --git a/nativescript.webpack.js b/nativescript.webpack.js index 1a17276..f331844 100644 --- a/nativescript.webpack.js +++ b/nativescript.webpack.js @@ -3,11 +3,37 @@ const { getPlatformName, } = require('@nativescript/webpack/dist/helpers/platform'); +// Solid 2 publishes an exports map whose `node` condition points at the SSR +// build. Resolving through it leaves the app with a renderer that never applies +// updates, so every entry point is pinned to an explicit browser bundle. `$` +// keeps each alias exact so subpaths still resolve normally. +const solidAliases = (production) => { + const solidDist = path.join( + path.dirname(require.resolve('solid-js/package.json')), + 'dist' + ); + const signalsDist = path.join( + path.dirname(require.resolve('@solidjs/signals/package.json')), + 'dist' + ); + const universalDist = path.dirname(require.resolve('@solidjs/universal')); + + return { + 'solid-js$': path.join(solidDist, production ? 'solid.js' : 'dev.js'), + '@solidjs/signals$': path.join( + signalsDist, + production ? 'prod/index.js' : 'dev.js' + ), + '@solidjs/universal$': path.join( + universalDist, + production ? 'universal.js' : 'dev.js' + ), + }; +}; + const solid = (config, env) => { const platform = getPlatformName(); - const solidPath = path.resolve(require.resolve("solid-js"), "../.."); - config.resolve.extensions .prepend('.js') .prepend('.ts') @@ -18,23 +44,10 @@ const solid = (config, env) => { .prepend(`.${platform}.jsx`) .prepend(`.${platform}.tsx`); - config.resolve.alias - .set( - 'solid-js/universal', - path.resolve(solidPath, `universal/dist/${env.production ? 'universal' : 'dev'}.js`) - ) - .set( - 'solid-js/store', - path.resolve(solidPath, `store/dist/${env.production ? 'store' : 'dev'}.js`) - ) - .set( - 'solid-js', - path.resolve(solidPath, `dist/${env.production ? 'solid' : 'dev'}.js`) - ) - .set( - 'solid-js/web', - path.resolve(solidPath, `dist/${env.production ? 'web' : 'dev'}.js`) - ); + const aliases = solidAliases(env.production); + for (const [key, value] of Object.entries(aliases)) { + config.resolve.alias.set(key, value); + } config.module .rule('bundle-source') @@ -62,11 +75,6 @@ const solid = (config, env) => { ['@babel/plugin-proposal-decorators', { legacy: true }], ['@babel/plugin-proposal-class-properties', { loose: true }] ], - env: { - development: { - plugins: [['solid-refresh/babel', { bundler: 'webpack5' }]], - }, - }, }); if (!env.production) { @@ -77,4 +85,3 @@ const solid = (config, env) => { }; module.exports = webpack => webpack.chainWebpack(solid); - diff --git a/package.json b/package.json index 3d730b6..cf8e999 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nativescript-community/solid-js", - "version": "0.1.2", + "version": "2.0.0-rc.0", "description": "SolidJS to work with NativeScript", "main": "src/index.js", "types": "index.d.ts", @@ -25,9 +25,10 @@ }, "peerDependencies": { "@babel/preset-typescript": "^7.23.3", - "babel-preset-solid": "^1.8.9", + "@solidjs/universal": "^2.0.0-rc.0", + "babel-preset-solid": "^2.0.0-rc.0", "dominative": "^0.1.2", - "solid-js": "^1.8.11" + "solid-js": "^2.0.0-rc.0" }, "publishConfig": { "access": "public" diff --git a/src/renderer.js b/src/renderer.js index bd049ed..4ccd962 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -1,22 +1,37 @@ import './dom.js' -import { createRenderer } from 'solid-js/universal' +import { createRenderer } from '@solidjs/universal' +// eslint-disable-next-line max-params +const setProperty = (node, name, value, prev) => { + if (name === 'style') return Object.assign(node.style, value) + if (value === prev) return -export const { - render, - effect, - memo, - createComponent, - createElement, - createTextNode, - insertNode, - insert, - spread, - setProp, - mergeProps -} = createRenderer({ - createElement(string) { - return document.createElement(string) + if (name.startsWith('on:')) { + const eventName = name.slice(3) + if (prev) node.removeEventListener(eventName, prev) + if (value) node.addEventListener(eventName, value) + } else if (name.startsWith('oncapture:')) { + const eventName = name.slice(10) + if (prev) node.removeEventListener(eventName, prev, true) + if (value) node.addEventListener(eventName, value, true) + } else { + if (process.env.NODE_ENV !== 'production' && name.startsWith('on')) { + console.warn(`[DOMiSOLID] Can not register '${name}' as an event handler. + For event handlers, pleas use 'on:raw-eventName' or 'oncapture:rawEvent-name'. + Event delegation isn't supported, also event names are case sensitive on NativeScript. + Refer to https://github.com/SudoMaker/dominative#tweaking to learn how to enable event bubbling and capturing.`) + } + node.setAttribute(name, value) + } +} + +const renderer = createRenderer({ + createElement(tagName, staticProps) { + const node = document.createElement(tagName) + if (staticProps) { + for (const name in staticProps) setProperty(node, name, staticProps[name]) + } + return node }, createTextNode(value) { return document.createTextNode(value) @@ -24,32 +39,7 @@ export const { replaceText(textNode, value) { textNode.nodeValue = value }, - // eslint-disable-next-line max-params - setProperty(node, name, value, prev) { - if (name === 'style') return Object.assign(node.style, value) - if (value === prev) return - - if (name === 'ref') return value(node) - - if (name.startsWith('on:')) { - const eventName = name.slice(3) - if (prev) node.removeEventListener(eventName, prev) - if (value) node.addEventListener(eventName, value) - } else if (name.startsWith('oncapture:')) { - const eventName = name.slice(10) - if (prev) node.removeEventListener(eventName, prev, true) - if (value) node.addEventListener(eventName, value, true) - } else { - if (process.env.NODE_ENV !== 'production' && name.startsWith('on')) { - console.warn(`[DOMiSOLID] Can not register '${name}' as an event handler. - For event handlers, pleas use 'on:raw-eventName' or 'oncapture:rawEvent-name'. - Event delegation isn't supported, also event names are case sensitive on NativeScript. - Refer to https://www.solidjs.com/docs/latest/api#on___oncapture___ for details about 'on:___' and 'oncapture:___'. - Refer to https://github.com/SudoMaker/dominative#tweaking to learn how to enable event bubbling and capturing.`) - } - node.setAttribute(name, value) - } - }, + setProperty, insertNode(parent, node, anchor) { parent.insertBefore(node, anchor) }, @@ -70,18 +60,43 @@ export const { } }); -export function use(fn, args) { - return fn?.(args); +export const { + render, + effect, + memo, + createComponent, + createElement, + createTextNode, + insertNode, + spread, + setProp, + mergeProps, + ref, + applyRef +} = renderer + +// Give every compiled insertion point a private anchor. Sibling blocks compiled +// against the same following element otherwise collapse onto it once one of them +// renders empty — the runtime drops the sentinel it made for that block and the +// remaining ones then append in flush order rather than source order. +// An empty text node is what the runtime itself uses to hold an empty slot, and +// DOMiNATIVE logs every comment node it sees. +// eslint-disable-next-line max-params +export const insert = (parent, accessor, marker, initial) => { + if (marker === undefined) return renderer.insert(parent, accessor, marker, initial) + const anchor = document.createTextNode('') + parent.insertBefore(anchor, marker) + return renderer.insert(parent, accessor, anchor, initial) } // Forward Solid control flow export { For, + Repeat, Show, - Suspense, - SuspenseList, Switch, Match, - Index, - ErrorBoundary -} from "solid-js" \ No newline at end of file + Loading, + Reveal, + Errored +} from "solid-js"