diff --git a/.changeset/jsx-tokenizer-solid2-migration.md b/.changeset/jsx-tokenizer-solid2-migration.md new file mode 100644 index 000000000..ad399a847 --- /dev/null +++ b/.changeset/jsx-tokenizer-solid2-migration.md @@ -0,0 +1,26 @@ +--- +"@solid-primitives/jsx-tokenizer": major +--- + +Migrate to Solid.js v2.0 (beta.10) + +## Breaking Changes + +**Peer dependencies**: `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10` are now required. + +### API changes + +- `isServer` is now imported from `@solidjs/web` (not `solid-js/web`) +- `JSX` types are now imported from `@solidjs/web` +- `ResolvedJSXElement` type renamed to `ResolvedElement` (from `solid-js`) in `resolveTokens` overloads +- `renderToString` in SSR tests moved to `@solidjs/web` + +### Usage changes + +- `createEffect` now requires the split compute/apply form — update any `createEffect` calls in consuming code +- Context is now its own provider: `` replaces `` +- `classList` is replaced by the `class` object/array form + +### Vitest config + +- Added `moduleName: "@solidjs/web"` to the shared vitest config `solid` option so JSX transforms target `@solidjs/web` instead of the removed `solid-js/web` subpath. This affects all packages with `.tsx` test files. diff --git a/configs/vitest.config.ts b/configs/vitest.config.ts index 611f49a6f..f2d2194a2 100644 --- a/configs/vitest.config.ts +++ b/configs/vitest.config.ts @@ -23,7 +23,7 @@ export default defineConfig(({ mode }) => { // https://github.com/solidjs/solid-refresh/issues/29 hot: false, // For testing SSR we need to do a SSR JSX transform - solid: { generate: testSSR ? "ssr" : "dom", omitNestedClosingTags: false }, + solid: { generate: testSSR ? "ssr" : "dom", omitNestedClosingTags: false, moduleName: "@solidjs/web" }, }), ], test: { diff --git a/packages/jsx-tokenizer/README.md b/packages/jsx-tokenizer/README.md index aae49a85e..2c46638aa 100644 --- a/packages/jsx-tokenizer/README.md +++ b/packages/jsx-tokenizer/README.md @@ -124,7 +124,7 @@ function Tabs(props: { children: (Tab: Component<{ value: T }>) => JSX.Elemen return (
    - {token =>
  • {token.data}
  • } + {token =>
  • {token.data}
  • }
); @@ -163,13 +163,14 @@ import { resolveTokens } from "@solid-primitives/jsx-tokenizer"; const tokens = resolveTokens(tokenizer, () => props.children); -createEffect(() => { - tokens().forEach(token => { +createEffect( + () => tokens(), + tokens => { // token is a function that returns the JSX Element fallback // token.data is the data returned by the tokenData function - console.log(token.data); - }); -}); + tokens.forEach(token => console.log(token.data)); + } +); // the return value of resolveTokens can be used in JSX (will render the fallback JSX Elements) return <>{els()}; @@ -188,17 +189,20 @@ const els = resolveTokens(tokenizer, () => props.children, { includeJSXElements: true, }); -createEffect(() => { - els().forEach(el => { - if (!isToken(tokenizer, el)) { - // el is a normal JSX Element - return; - } - // token is a function that returns the JSX Element fallback - // token.data is the data returned by the tokenData function - console.log(token.data); - }); -}); +createEffect( + () => els(), + els => { + els.forEach(el => { + if (!isToken(tokenizer, el)) { + // el is a normal JSX Element + return; + } + // token is a function that returns the JSX Element fallback + // token.data is the data returned by the tokenData function + console.log(el.data); + }); + } +); // the return value of resolveTokens can be used in JSX return <>{els()}; @@ -221,7 +225,7 @@ Since `resolveTokens` is eagerly resolving the JSX structure, if you want to pro ```tsx function ParentComponent(props) { return ( - + {untrack(() => { const tokens = resolveTokens(tokenizer, () => props.children); @@ -229,7 +233,7 @@ function ParentComponent(props) { return <>{tokens()}; })} - +
); } ``` @@ -243,13 +247,13 @@ For example, [`@solidjs/router`](https://github.com/solidjs/solid-router) which function App() { return ( - + {/* component prop is not rendered immediately, it is rendered within as later time, so the context will not be available in Home component */} - + ); } @@ -262,11 +266,11 @@ function Home() { // do this instead function App() { return ( - + - + ); } ``` @@ -291,10 +295,6 @@ token; // token is typed as UnionOfAcceptedTokens isToken([tokenizer1, tokenizer2, MyTokenComponent], token); ``` -## Demo - -[Live Example](https://primitives.solidjs.community/playground/jsx-tokenizer) | [Source Code](./dev/index.tsx) - ## Changelog See [CHANGELOG.md](./CHANGELOG.md) diff --git a/packages/jsx-tokenizer/dev/index.tsx b/packages/jsx-tokenizer/dev/index.tsx deleted file mode 100644 index 4e4b8f5bc..000000000 --- a/packages/jsx-tokenizer/dev/index.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { type Component, type JSX, type ParentComponent } from "solid-js"; - -import { createTokenizer, createToken, isToken, resolveTokens } from "../src/index.js"; - -type Props = { - value: number; - children?: JSX.Element; -}; - -const parser = createTokenizer<{ - id: "Value" | "Add"; - props: Props; -}>({ name: "calculator" }); - -const Calculator: ParentComponent = props => { - const tokens = resolveTokens([parser, Subtract], () => props.children, { - includeJSXElements: true, - }); - - const calculation = () => { - let result = 0; - tokens().forEach(el => { - if (!isToken([parser, Subtract], el)) { - console.info("not a token element:", el); - return; - } - console.info("token element:", el); - const data = el.data; - console.info("token data is", data); - if (data.id === "Value") { - result = data.props.value; - } else if (data.id === "Add") { - result += data.props.value; - } else if (data.id === "Subtract") { - result -= data.props.value; - } - console.info("result is", result); - }); - return result; - }; - - return ( -
- {tokens()} = {calculation()} -
- ); -}; - -const Value = createToken( - parser, - (props: Props) => ({ - props, - id: "Value", - }), - props => <>{props.value}, -); - -const Add = createToken( - parser, - (props: Props) => ({ - props, - id: "Add", - }), - props => <> + {props.value}, -); - -const Subtract = createToken( - (props: Props) => ({ - props: props as Props, - id: "Subtract" as const, - }), - props => <> - {props.value}, -); - -const App: Component = () => { - return ( -
-
-

This is a calculator

-
- -

- (I'am not a token) -

- - - -
-
-
-
- ); -}; - -export default App; diff --git a/packages/jsx-tokenizer/package.json b/packages/jsx-tokenizer/package.json index 2df2782b7..4f00fef2a 100644 --- a/packages/jsx-tokenizer/package.json +++ b/packages/jsx-tokenizer/package.json @@ -1,6 +1,6 @@ { "name": "@solid-primitives/jsx-tokenizer", - "version": "1.1.3", + "version": "2.0.0", "description": "A primitive to tokenize your solid-components to enable custom parsing.", "author": "Vincent Van Dijck ", "contributors": [ @@ -60,9 +60,11 @@ "@solid-primitives/utils": "workspace:^" }, "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.14", + "solid-js": "^2.0.0-beta.14" }, "devDependencies": { - "solid-js": "^1.9.7" + "@solidjs/web": "2.0.0-beta.14", + "solid-js": "2.0.0-beta.14" } } diff --git a/packages/jsx-tokenizer/src/index.ts b/packages/jsx-tokenizer/src/index.ts index a7c337b2c..274c25a19 100644 --- a/packages/jsx-tokenizer/src/index.ts +++ b/packages/jsx-tokenizer/src/index.ts @@ -3,11 +3,10 @@ import { type Component, createComponent, createMemo, - type JSX, DEV, - type ResolvedJSXElement, + type ResolvedElement, } from "solid-js"; -import { isServer } from "solid-js/web"; +import { isServer, type JSX } from "@solidjs/web"; import type { NoInfer, Many } from "@solid-primitives/utils"; import { asArray } from "@solid-primitives/utils"; @@ -178,7 +177,7 @@ export function resolveTokens( options: { includeJSXElements: true; }, -): Accessor<(TokenElement | ResolvedJSXElement)[]>; +): Accessor<(TokenElement | ResolvedElement)[]>; export function resolveTokens[]>( tokenizers: TTokenizers, @@ -194,7 +193,7 @@ export function resolveTokens[]>( options: { includeJSXElements: true; }, -): Accessor<(TokenElement> | ResolvedJSXElement)[]>; +): Accessor<(TokenElement> | ResolvedElement)[]>; export function resolveTokens( tokenizers: Many>, @@ -202,7 +201,7 @@ export function resolveTokens( options?: { includeJSXElements?: boolean; }, -): Accessor<(TokenElement | ResolvedJSXElement)[]> { +): Accessor<(TokenElement | ResolvedElement)[]> { const symbols = new Set(asArray(tokenizers).map(p => p[$TOKENIZER])); const children = createMemo(fn); return createMemo(() => getResolvedTokens([], children(), symbols, options?.includeJSXElements)); diff --git a/packages/jsx-tokenizer/stories/jsx-tokenizer.stories.tsx b/packages/jsx-tokenizer/stories/jsx-tokenizer.stories.tsx new file mode 100644 index 000000000..98fbdb981 --- /dev/null +++ b/packages/jsx-tokenizer/stories/jsx-tokenizer.stories.tsx @@ -0,0 +1,432 @@ +import { createSignal, For, Show } from "solid-js"; +import preview from "../../../.storybook/preview.js"; +import { createTokenizer, createToken, resolveTokens, isToken } from "@solid-primitives/jsx-tokenizer"; +import readme from "../README.md?raw"; +import { + Container, + Card, + Button, + ButtonRow, + Kbd, + Badge, + colors, + font, + radii, +} from "../../../.storybook/ui/index.js"; + +const meta = preview.meta({ + title: "Control Flow/JSX Tokenizer", + tags: ["autodocs"], + parameters: { + layout: "centered", + docs: { + description: { + component: readme, + }, + }, + }, +}); + +export default meta; + +// ── Story 1: Tab bar from token children ────────────────────────────────────── + +type TabData = { id: string; label: string }; +const TabTokenizer = createTokenizer({ name: "tabs" }); +const Tab = createToken( + TabTokenizer, + (props: TabData) => props, + props => {props.label}, +); + +const PANELS: Record = { + overview: "High-level summary of the project state and recent metrics.", + settings: "Configure preferences, integrations, and notification rules.", + activity: "A chronological feed of recent changes and automated events.", +}; + +export const TabBarFromTokens = meta.story({ + name: "Tab bar from token children", + parameters: { + docs: { + description: { + story: + "`resolveTokens` extracts `` data from children without rendering them. The parent builds the tab bar entirely from `token.data` — children declare labels only, the parent controls the layout and active state.", + }, + }, + }, + render: () => { + const [active, setActive] = createSignal("overview"); + + const tokens = resolveTokens(TabTokenizer, () => ( + <> + + + + + )); + + return ( + +
+ + {token => ( + + )} + +
+ + +

+ {PANELS[active()] ?? ""} +

+
+ +

+ The parent reads token.data to render — children are never mounted as DOM. +

+
+ ); + }, +}); + +// ── Story 2: Token as its own tokenizer (standalone token) ──────────────────── + +type CrumbData = { label: string; href?: string }; +const Crumb = createToken( + (props: CrumbData) => props, + props => {props.label}, +); + +const PATHS = [ + ["Home"], + ["Home", "Projects"], + ["Home", "Projects", "Solid Primitives"], + ["Home", "Projects", "Solid Primitives", "jsx-tokenizer"], +]; + +export const StandaloneToken = meta.story({ + name: "Standalone token (no tokenizer)", + parameters: { + docs: { + description: { + story: + "`createToken` without a tokenizer argument creates its own identity. The token component can then be passed directly to `resolveTokens` as the tokenizer — no separate `createTokenizer` call needed.", + }, + }, + }, + render: () => { + const [depth, setDepth] = createSignal(2); + const crumbs = () => PATHS[depth()] ?? []; + + const tokens = resolveTokens(Crumb, () => ( + {(label, i) => } + )); + + return ( + +

+ Breadcrumb +

+ + +
+ + {(token, i) => ( + <> + 0}> + / + + { + if (i() < tokens().length - 1) setDepth(i()); + }} + > + {token.data.label} + + + )} + +
+
+ + + + + + +

+ Crumb is the tokenizer — no separate createTokenizer call. +

+
+ ); + }, +}); + +// ── Story 3: Tokens mixed with plain JSX (includeJSXElements) ───────────────── + +type CalloutData = { kind: "info" | "tip" | "warning"; text: string }; +const CalloutTokenizer = createTokenizer({ name: "callout" }); +const Callout = createToken( + CalloutTokenizer, + (props: CalloutData) => ({ kind: props.kind, text: props.text }), + props =>
{props.text}
, +); + +const CALLOUT_STYLE: Record< + CalloutData["kind"], + { bg: string; border: string; color: string; label: string } +> = { + info: { bg: "#eff6ff", border: "#93c5fd", color: "#1d4ed8", label: "Info" }, + tip: { bg: "#f0fdf4", border: "#86efac", color: "#15803d", label: "Tip" }, + warning: { bg: "#fffbeb", border: "#fcd34d", color: "#b45309", label: "Warning" }, +}; + +export const MixedTokensAndJSX = meta.story({ + name: "Tokens mixed with plain JSX", + parameters: { + docs: { + description: { + story: + "With `{ includeJSXElements: true }`, `resolveTokens` collects both tokens and regular JSX. Use `isToken` to narrow each item: tokens get the styled callout treatment, everything else renders as plain prose.", + }, + }, + }, + render: () => { + const items = resolveTokens( + CalloutTokenizer, + () => ( + <> +

This component resolves children with includeJSXElements: true.

+ +

Plain JSX passes through and renders as-is.

+ + + + ), + { includeJSXElements: true }, + ); + + return ( + +
+ + {item => { + if (isToken(CalloutTokenizer, item)) { + const s = CALLOUT_STYLE[item.data.kind]; + return ( +
+ + {s.label} + + {item.data.text} +
+ ); + } + return ( +
+ {item as any} +
+ ); + }} +
+
+ +

+ Tokens are intercepted; plain JSX passes through unchanged. +

+
+ ); + }, +}); + +// ── Story 4: Array of tokenizers ────────────────────────────────────────────── + +type ActionData = { label: string; kbd?: string; description?: string; danger?: boolean }; +const ActionTokenizer = createTokenizer({ name: "action" }); +const Action = createToken( + ActionTokenizer, + (props: ActionData) => ({ + label: props.label, + kbd: props.kbd, + description: props.description, + danger: props.danger, + }), + props =>
{props.label}
, +); + +const Div = createToken( + (_props: {}) => ({}), + () =>
, +); + +export const MultipleTokenizers = meta.story({ + name: "Resolving two token families", + parameters: { + docs: { + description: { + story: + "Pass an array to `resolveTokens` to collect tokens from multiple tokenizers in a single pass. Use `isToken` on each tokenizer to narrow which kind of token you're rendering.", + }, + }, + }, + render: () => { + const [log, setLog] = createSignal([]); + const push = (msg: string) => setLog(l => [msg, ...l].slice(0, 4)); + + const tokens = resolveTokens([ActionTokenizer, Div], () => ( + <> + + + +
+ + +
+ + + )); + + return ( + +
+ + {token => { + if (isToken(Div, token)) { + return
; + } + if (isToken(ActionTokenizer, token)) { + const d = token.data; + return ( + + ); + } + return null; + }} + +
+ + 0}> +
+
+ Last triggered: +
+ + {(entry, i) => ( +
+ {entry} +
+ )} +
+
+
+ +

+ Action and Div tokens are distinct families, resolved in one pass. +

+ + ); + }, +}); diff --git a/packages/jsx-tokenizer/test/index.test.tsx b/packages/jsx-tokenizer/test/index.test.tsx index ee3b1bbd4..105f69b36 100644 --- a/packages/jsx-tokenizer/test/index.test.tsx +++ b/packages/jsx-tokenizer/test/index.test.tsx @@ -1,4 +1,4 @@ -import { children, createRoot, createSignal, Show } from "solid-js"; +import { children, createRoot, createSignal, flush, Show } from "solid-js"; import { describe, expect, it } from "vitest"; import { createTokenizer, @@ -70,24 +70,27 @@ describe("jsx-tokenizer", () => { }); it("handled reactive children", () => { - createRoot(() => { - const [show, setShow] = createSignal(true); + const [show, setShow] = createSignal(true); - const tokens = resolveTokens(parser1, () => ( + const { tokens, dispose } = createRoot(dispose => ({ + tokens: resolveTokens(parser1, () => ( <> - )); + )), + dispose, + })); - expect(tokens()).toHaveLength(2); + expect(tokens()).toHaveLength(2); - setShow(false); + setShow(false); + flush(); + expect(tokens()).toHaveLength(1); - expect(tokens()).toHaveLength(1); - }); + dispose(); }); it("should render tokens", () => { diff --git a/packages/jsx-tokenizer/test/server.test.tsx b/packages/jsx-tokenizer/test/server.test.tsx index 9383ce3e2..a7a1292c6 100644 --- a/packages/jsx-tokenizer/test/server.test.tsx +++ b/packages/jsx-tokenizer/test/server.test.tsx @@ -1,4 +1,4 @@ -import { renderToString } from "solid-js/web"; +import { renderToString } from "@solidjs/web"; import { describe, expect, it } from "vitest"; import { createTokenizer, createToken, resolveTokens } from "../src/index.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2921a78a4..1e8db7031 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -561,9 +561,12 @@ importers: specifier: workspace:^ version: link:../utils devDependencies: + '@solidjs/web': + specifier: 2.0.0-beta.14 + version: 2.0.0-beta.14(solid-js@2.0.0-beta.14) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.14 + version: 2.0.0-beta.14 packages/keyboard: dependencies: