diff --git a/.changeset/router-solid-draft.md b/.changeset/router-solid-draft.md
new file mode 100644
index 0000000..18d35a2
--- /dev/null
+++ b/.changeset/router-solid-draft.md
@@ -0,0 +1,9 @@
+---
+'@effector/router-solid': minor
+---
+
+Add SolidJS bindings package `@effector/router-solid` (draft). Mirrors the
+`@effector/router-react` API — `RouterProvider`, `createRouteView`,
+`createLazyRouteView`, `createRoutesView`, `Link`/`useLink`, `Outlet`,
+`withLayout`, `useRouter`, `useIsOpened`, `useOpenedViews` — implemented with
+Solid primitives. Supports only the current major of SolidJS (1.x).
diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml
index 0b04a5e..1fe427d 100644
--- a/.github/workflows/pr-checks.yml
+++ b/.github/workflows/pr-checks.yml
@@ -74,3 +74,30 @@ jobs:
path: ./packages
- run: pnpm run test
+
+ solid-e2e:
+ name: Solid router integration
+ needs: build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup environment
+ uses: ./.github/actions/setup
+
+ - uses: actions/download-artifact@v4
+ with:
+ name: build-output
+ path: ./packages
+
+ - name: Install Chromium
+ run: pnpm --filter @effector/router-solid-example exec playwright install --with-deps chromium
+
+ - name: Build Solid example
+ run: pnpm --filter @effector/router-solid-example build
+
+ - name: Typecheck Solid example
+ run: pnpm --filter @effector/router-solid-example typecheck
+
+ - name: Run Solid browser tests
+ run: pnpm --filter @effector/router-solid-example test:e2e
diff --git a/.gitignore b/.gitignore
index b9c1fb0..de67d18 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ node_modules
dist
cache
dist-ssr
+examples/solid-router/test-results/
*.local
.husky
diff --git a/README.md b/README.md
index 10e23ff..fa297fb 100644
--- a/README.md
+++ b/README.md
@@ -13,12 +13,13 @@ A type-safe, framework-agnostic router built on top of [Effector](https://effect
| [`@effector/router-paths`](packages/paths) | Path parsing and matching utilities. |
| [`@effector/router-react`](packages/react) | React bindings for the router. |
| [`@effector/router-react-native`](packages/react-native) | React Native bindings for the router. |
+| [`@effector/router-solid`](packages/solid) | SolidJS bindings for the router (draft). |
## Documentation
Full documentation is published at [router.effector.dev](https://router.effector.dev). Each package also
-has its own README: [core](packages/core), [paths](packages/paths), [react](packages/react), and
-[react-native](packages/react-native).
+has its own README: [core](packages/core), [paths](packages/paths), [react](packages/react), [react-native](packages/react-native), and
+[solid](packages/solid).
## Contributing
diff --git a/SOLID_CORE_API_GAPS.md b/SOLID_CORE_API_GAPS.md
new file mode 100644
index 0000000..09726d5
--- /dev/null
+++ b/SOLID_CORE_API_GAPS.md
@@ -0,0 +1,94 @@
+# Solid and Core API gaps
+
+This file records API and typing gaps discovered while building the SolidJS
+consumer application in `examples/solid-router`. The items are intentionally
+kept separate from the integration-test plan so they can be fixed and removed
+independently.
+
+## Confirmed issues
+
+### 1. Parent route params are missing from child route types
+
+`createRoute({path: '/tasks/:taskId', parent: projectRoute})` infers only the
+child segment. The built route still needs the parent `projectId`, so consumer
+code has to cast params when using `Link` or `useLink`.
+
+Impact:
+
+- child links are not fully type-safe;
+- route builders and route state expose different parameter contracts;
+- the Solid consumer app needed `as any` for nested links.
+
+Suggested direction: derive the child route params as the intersection of the
+parent route params and the child path params, while preserving the current
+runtime builder behavior.
+
+### 2. `VirtualRoute` is not assignable to `Route`
+
+`group`, `createVirtualRoute`, and `chainRoute` return `VirtualRoute`, but the
+Solid helpers accept `Route | Router`. The types differ notably in the
+payload of `open` and in `$params`, so valid virtual routes require casts when
+passed to `useIsOpened`, `createRouteView`, or related helpers.
+
+Suggested direction: introduce a shared public route/viewable-route contract
+for path routes, pathless routes, and virtual routes, or widen the Solid helper
+signatures to the exact virtual-route shape.
+
+### 3. `withLayout` drops nested route children
+
+`withLayout` maps each view to a new object containing only `route` and
+`view`. A `RouteView.children` tree is lost when a parent view is wrapped by a
+layout.
+
+Impact: a parent route wrapped with `withLayout` cannot reliably render its
+nested `Outlet` tree.
+
+Suggested direction: preserve `children` when mapping the views and add a
+regression test for a layout-wrapped parent with an active child route.
+
+### 4. `createLazyRouteView` dropped nested route children (resolved)
+
+`CreateLazyRouteViewProps` allowed `children`, but the Solid implementation
+returned only `route` and `view`. Lazy parent routes therefore lost their
+nested outlet configuration. The Solid binding now returns `children` and has a
+regression test covering the contract.
+
+The same runtime gap remains in the React binding and should be fixed there
+separately.
+
+### 5. `Link.href` does not include the query payload
+
+`Link` resolves `href` from the route path only. Its `query` prop is applied on
+click through `onOpen`, but is absent from the rendered anchor href.
+
+Impact: copy-link, native link preview, accessibility tooling, and no-script
+fallbacks see an incomplete URL.
+
+Suggested direction: expose a URL builder that serializes both route params
+and query, then use it for `href` while keeping click navigation behavior.
+
+## Follow-up checks
+
+### 6. Lazy fallback behavior needs an explicit browser contract
+
+The core route waits for the async bundle import before opening the route, and
+the Solid binding also wraps the component in `Suspense`. A browser test should
+define whether the fallback is expected during bundle loading or only during
+component rendering, then lock that behavior down. In the current consumer
+example, a direct deep-link to `/reports` can remain on the router fallback,
+while entering the same lazy route through client-side navigation renders it
+correctly. The E2E suite records the working client-navigation behavior and
+keeps direct-refresh coverage on a non-lazy nested route.
+
+### 7. Active-link styling is not part of `Link`
+
+The Solid `Link` API forwards anchor props but does not expose an active-route
+state or `activeClass`. This is not a runtime bug, but every consumer must
+implement active styling separately. Decide whether this is intentional parity
+with the current React binding or a future API addition.
+
+## Verification context
+
+These findings came from the Solid consumer app and package declarations, not
+from speculative API review. The app keeps casts localized to the scenarios
+above so the integration tests still exercise the runtime behavior directly.
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 4846817..e13f430 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -104,6 +104,11 @@ export default defineConfig({
},
],
},
+ {
+ text: 'Solid β',
+ collapsed: false,
+ items: [{ text: 'Overview', link: '/solid/' }],
+ },
],
footer: {
diff --git a/docs/solid/index.md b/docs/solid/index.md
new file mode 100644
index 0000000..f9306ad
--- /dev/null
+++ b/docs/solid/index.md
@@ -0,0 +1,152 @@
+# Solid
+
+SolidJS bindings for effector/router, providing components and reactive helpers for seamless integration.
+
+::: warning Draft
+`@effector/router-solid` is an early draft. Its API mirrors [`@effector/router-react`](/react/)
+and may change before the first stable release. Only the current stable major of SolidJS (**1.x**)
+is supported.
+:::
+
+## Overview
+
+`@effector/router-solid` provides Solid-specific utilities to use effector/router in your Solid applications:
+
+- **Route Views** - Connect routes to Solid components
+- **Navigation Components** - `Link` and navigation helpers
+- **Reactive helpers** - Access router state as Solid accessors
+- **Layouts** - Wrap multiple routes with shared layouts
+
+Unlike the React binding, reactive helpers (`useLink`, `useIsOpened`, `useOpenedViews`) return Solid
+**accessors** (`() => value`) instead of plain values.
+
+## Installation
+
+```bash
+npm install @effector/router-solid @effector/router effector effector-solid solid-js
+```
+
+## Quick Example
+
+```tsx
+import { createRoute, createRouter } from '@effector/router';
+import {
+ RouterProvider,
+ createRouteView,
+ createRoutesView,
+ Link,
+} from '@effector/router-solid';
+
+// 1. Create routes
+const homeRoute = createRoute({ path: '/home' });
+const aboutRoute = createRoute({ path: '/about' });
+
+// 2. Create router
+const router = createRouter({ routes: [homeRoute, aboutRoute] });
+
+// 3. Bind routes to components
+const HomeScreen = createRouteView({
+ route: homeRoute,
+ view: () => (
+
+
Home
+ Go to About
+
+ ),
+});
+
+const AboutScreen = createRouteView({
+ route: aboutRoute,
+ view: () => (
+
+
About
+ Go to Home
+
+ ),
+});
+
+// 4. Render the currently opened route
+const RoutesView = createRoutesView({
+ routes: [HomeScreen, AboutScreen],
+});
+
+// 5. Use in app
+function App() {
+ return (
+
+
+
+ );
+}
+```
+
+## Using Route Parameters
+
+Access route parameters with effector-solid's `useUnit`, which returns a Solid accessor:
+
+```tsx
+import { useUnit } from 'effector-solid';
+
+const userRoute = createRoute({ path: '/user/:id' });
+
+const UserScreen = createRouteView({
+ route: userRoute,
+ view: () => {
+ const params = useUnit(userRoute.$params);
+ return User ID: {params().id}
;
+ },
+});
+```
+
+## Lazy Loading
+
+Load route components on demand:
+
+```tsx
+import { createLazyRouteView } from '@effector/router-solid';
+
+const ProfileScreen = createLazyRouteView({
+ route: profileRoute,
+ view: () => import('./screens/ProfileScreen'),
+ fallback: () => Loading...
,
+});
+```
+
+## Layouts
+
+Share layouts across multiple routes:
+
+```tsx
+import { withLayout } from '@effector/router-solid';
+
+const MainLayout = (props) => (
+
+
+ {props.children}
+
+
+);
+
+const RoutesView = createRoutesView({
+ routes: [
+ ...withLayout(MainLayout, [HomeScreen, AboutScreen, ContactScreen]),
+ LoginScreen, // Without layout
+ ],
+});
+```
+
+## APIs
+
+The Solid binding mirrors the React one:
+
+- `RouterProvider` — provide the router to the tree.
+- `createRouteView` / `createLazyRouteView` — bind a route to a component (with optional lazy loading).
+- `createRoutesView` — render the currently opened route, with an `otherwise` fallback.
+- `Link`, `useLink` — declarative and imperative navigation.
+- `withLayout` — share a layout across routes.
+- `Outlet`, `useRouter`, `useIsOpened`, `useOpenedViews` — composition helpers.
+
+## Next Steps
+
+- [Core Package](/core/create-router) - Learn about core concepts
+- [React bindings](/react/) - The stable reference implementation this binding mirrors
diff --git a/examples/solid-router/e2e/advanced.spec.ts b/examples/solid-router/e2e/advanced.spec.ts
new file mode 100644
index 0000000..f0c06a2
--- /dev/null
+++ b/examples/solid-router/e2e/advanced.spec.ts
@@ -0,0 +1,72 @@
+import { expect, test } from '@playwright/test';
+
+test('renders lazy route and nested settings router', async ({ page }) => {
+ await page.goto('/');
+ await page.getByRole('link', { name: 'Reports' }).click();
+ await expect(page).toHaveURL(/\/reports$/);
+ await expect(page.getByTestId('page-reports')).toBeVisible();
+ await expect(page.locator('body')).toHaveAttribute(
+ 'data-reports-mounted',
+ 'true',
+ );
+
+ await page.getByRole('link', { name: 'Settings' }).click();
+ await expect(page).toHaveURL(/\/settings$/);
+ await expect(page.getByTestId('settings-general')).toBeVisible();
+
+ await page.getByRole('link', { name: 'Profile' }).click();
+ await expect(page).toHaveURL(/\/settings\/profile$/);
+ await expect(page.getByTestId('settings-profile')).toBeVisible();
+});
+
+test('guards a route and recovers after authorization changes', async ({
+ page,
+}) => {
+ await page.goto('/protected');
+ await expect(page).toHaveURL(/\/protected$/);
+ await expect(page.getByTestId('page-not-found')).toBeVisible();
+
+ await page.getByTestId('toggle-auth').click();
+ await page.getByTestId('retry-protected').click();
+ await expect(page.getByTestId('page-protected')).toBeVisible();
+ await expect(page.getByTestId('auth-state')).toContainText('enabled');
+});
+
+test('uses a query-backed modal router without changing the host path', async ({
+ page,
+}) => {
+ await page.goto('/projects/solid?sort=asc');
+ await page.getByTestId('open-modal').click();
+ await expect(page).toHaveURL(
+ /\/projects\/solid\?sort=asc&modal=%2Ftask%2Froute-contracts$/,
+ );
+ await expect(page.getByTestId('task-modal')).toContainText('Route contracts');
+
+ await page.getByTestId('close-modal').click();
+ await expect(page).toHaveURL(/\/projects\/solid\?sort=asc$/);
+ await expect(page.getByTestId('task-modal')).toBeHidden();
+});
+
+test('opens and closes a virtual drawer route', async ({ page }) => {
+ await page.goto('/');
+ await page.getByTestId('open-drawer').click();
+ await expect(page.getByTestId('activity-drawer')).toContainText('activity');
+ await page.getByTestId('close-drawer').click();
+ await expect(page.getByTestId('activity-drawer')).toBeHidden();
+});
+
+test('falls back for an unknown URL and supports direct refresh paths', async ({
+ page,
+}) => {
+ await page.goto('/does-not-exist');
+ await expect(page.getByTestId('page-not-found')).toBeVisible();
+
+ await page.goto('/projects/router/tasks/route-contracts');
+ await expect(page.getByTestId('project-task')).toContainText(
+ 'Route contracts',
+ );
+ await page.reload();
+ await expect(page.getByTestId('project-task')).toContainText(
+ 'Route contracts',
+ );
+});
diff --git a/examples/solid-router/e2e/navigation.spec.ts b/examples/solid-router/e2e/navigation.spec.ts
new file mode 100644
index 0000000..2dc12b7
--- /dev/null
+++ b/examples/solid-router/e2e/navigation.spec.ts
@@ -0,0 +1,52 @@
+import { expect, test } from '@playwright/test';
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/');
+ await expect(page.getByTestId('page-home')).toBeVisible();
+});
+
+test('navigates through params, nested outlet, and custom useLink', async ({
+ page,
+}) => {
+ await page.getByTestId('project-router').click();
+ await expect(page).toHaveURL(/\/projects\/router\/overview$/);
+ await expect(page.getByTestId('project-overview')).toContainText('router');
+ await expect(page.getByTestId('project-group-state')).toHaveText('active');
+
+ await page.getByRole('link', { name: 'Tasks' }).click();
+ await expect(page).toHaveURL(/\/projects\/router\/tasks\/integration-tests$/);
+ await expect(page.getByTestId('project-task')).toContainText(
+ 'Integration tests',
+ );
+ await expect(page.getByTestId('custom-task-path')).toHaveText(
+ '/projects/solid/tasks/integration-tests',
+ );
+
+ await page.getByTestId('custom-task-link').click();
+ await expect(page).toHaveURL(/\/projects\/solid\/tasks\/integration-tests$/);
+ await expect(page.getByTestId('project-task')).toBeVisible();
+});
+
+test('handles search query tracker and history controls', async ({ page }) => {
+ await page.getByRole('link', { name: 'Search' }).click();
+ await expect(page).toHaveURL(/\/search\?q=solid$/);
+ await expect(page.getByTestId('search-value')).toContainText('solid');
+
+ await page.getByTestId('search-input').fill('router');
+ await page.getByTestId('search-submit').click();
+ await expect(page).toHaveURL(/\/search\?q=router$/);
+ await expect(page.getByTestId('search-value')).toContainText('router');
+
+ await page.getByTestId('search-clear').click();
+ await expect(page).toHaveURL(/\/search$/);
+ await expect(page.getByTestId('search-value')).toContainText('none');
+
+ await page.getByTestId('router-back').click();
+ await expect(page).toHaveURL(/\/search\?q=router$/);
+});
+
+test('supports the dynamically registered pathless route', async ({ page }) => {
+ await page.getByTestId('link-help').click();
+ await expect(page).toHaveURL(/\/help$/);
+ await expect(page.getByTestId('page-help')).toBeVisible();
+});
diff --git a/examples/solid-router/index.html b/examples/solid-router/index.html
new file mode 100644
index 0000000..002bf21
--- /dev/null
+++ b/examples/solid-router/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Solid Router Lab
+
+
+
+
+
+
diff --git a/examples/solid-router/package.json b/examples/solid-router/package.json
new file mode 100644
index 0000000..f58aa03
--- /dev/null
+++ b/examples/solid-router/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@effector/router-solid-example",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0",
+ "build": "vite build",
+ "preview": "vite preview --host 0.0.0.0",
+ "typecheck": "tsc --noEmit -p tsconfig.json",
+ "test:e2e": "playwright test",
+ "test:e2e:ui": "playwright test --ui"
+ },
+ "dependencies": {
+ "@effector/router": "workspace:*",
+ "@effector/router-solid": "workspace:*",
+ "effector": "^23.4.4",
+ "effector-solid": "^0.23.0",
+ "history": "5.3.0",
+ "solid-js": "^1.9.14",
+ "zod": "^4.3.6"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.52.0",
+ "typescript": "5.8.2",
+ "vite": "7.3.2",
+ "vite-plugin-solid": "^2.11.12"
+ }
+}
diff --git a/examples/solid-router/playwright.config.ts b/examples/solid-router/playwright.config.ts
new file mode 100644
index 0000000..1a04d04
--- /dev/null
+++ b/examples/solid-router/playwright.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './e2e',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ reporter: process.env.CI ? 'github' : 'list',
+ use: {
+ baseURL: 'http://127.0.0.1:4174',
+ trace: 'retain-on-failure',
+ screenshot: 'only-on-failure',
+ video: 'retain-on-failure',
+ },
+ webServer: {
+ command: 'pnpm build && pnpm preview',
+ url: 'http://127.0.0.1:4174',
+ reuseExistingServer: !process.env.CI,
+ timeout: 120_000,
+ },
+ projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
+});
diff --git a/examples/solid-router/src/App.tsx b/examples/solid-router/src/App.tsx
new file mode 100644
index 0000000..a05ee4a
--- /dev/null
+++ b/examples/solid-router/src/App.tsx
@@ -0,0 +1,542 @@
+import { For, Show, createEffect, createSignal, type JSX } from 'solid-js';
+import { useUnit } from 'effector-solid';
+import {
+ createLazyRouteView,
+ createRouteView,
+ createRoutesView,
+ Link,
+ Outlet,
+ RouterProvider,
+ useIsOpened,
+ useLink,
+ useOpenedViews,
+ useRouter,
+ useRouterContext,
+ withLayout,
+} from '@effector/router-solid';
+import { z } from 'zod';
+import {
+ $authorized,
+ authChanged,
+ drawerRoute,
+ helpRoute,
+ homeRoute,
+ modalRouter,
+ modalTaskRoute,
+ projectGroup,
+ projectOverviewRoute,
+ projectTaskRoute,
+ projectsRoute,
+ protectedVisibleRoute,
+ reportsRoute,
+ router,
+ searchRoute,
+ settingsGeneralRoute,
+ settingsProfileRoute,
+ settingsRouter,
+} from './routing/routes';
+
+const projects: Record = {
+ router: { name: 'Effector Router', owner: 'Core team' },
+ solid: { name: 'Solid bindings', owner: 'UI team' },
+};
+
+const taskNames: Record = {
+ 'integration-tests': 'Integration tests',
+ 'route-contracts': 'Route contracts',
+};
+
+const searchTracker = router.trackQuery({
+ forRoutes: [searchRoute],
+ parameters: z.object({ q: z.string().min(1) }),
+});
+
+function MainLayout(props: { children: JSX.Element }) {
+ return (
+
+
+
+ Router Lab
+
+
+ Overview
+
+ Search
+
+ Reports
+ Settings
+
+
+ {props.children}
+
+ );
+}
+
+function HomePage() {
+ const openDrawer = useUnit(drawerRoute.open);
+
+ return (
+
+ Workspace
+ Project overview
+ A Solid consumer app for exercising router bindings.
+
+
+ {([id, project]) => (
+
+ Project
+ {project.name}
+ {project.owner}
+
+ Open project
+
+
+ )}
+
+
+
+
+ Help route
+
+ openDrawer({ panel: 'activity' })}
+ >
+ Open activity panel
+
+
+
+ );
+}
+
+function ProjectPage() {
+ const params = useUnit(projectsRoute.$params);
+ const active = useIsOpened(projectGroup as any);
+ const project = () =>
+ projects[params().projectId] ?? {
+ name: 'Unknown project',
+ owner: 'Unknown',
+ };
+
+ return (
+
+
+
+ Project
+
{project().name}
+
+
+ {active() ? 'active' : 'closed'}
+
+
+ {project().owner}
+
+
+ Overview
+
+
+ Tasks
+
+
+
+
+
+
+ );
+}
+
+function ProjectOverviewPage() {
+ const params = useUnit(projectsRoute.$params);
+ return (
+
+
Overview
+
+ Project id: {params().projectId}
+
+
+ );
+}
+
+function ProjectTaskPage() {
+ const params = useUnit(projectTaskRoute.$params);
+ const task = () => taskNames[params().taskId] ?? params().taskId;
+ const taskLink = useLink(
+ projectTaskRoute,
+ () =>
+ ({ projectId: 'solid', taskId: params().taskId }) as unknown as {
+ projectId: string;
+ taskId: string;
+ },
+ );
+
+ return (
+
+
{task()}
+
+ Task id: {params().taskId}
+
+
+ taskLink.onOpen({
+ params: { projectId: 'solid', taskId: params().taskId } as any,
+ })
+ }
+ >
+ Open in Solid project
+
+
+ {taskLink.path()}
+
+
+ );
+}
+
+function SearchPage() {
+ const currentQuery = useUnit(router.$query);
+ const enterSearch = useUnit(searchTracker.enter);
+ const exitSearch = useUnit(searchTracker.exit);
+ const [term, setTerm] = createSignal('solid');
+
+ createEffect(() => {
+ const value = currentQuery().q;
+ if (typeof value === 'string') setTerm(value);
+ });
+
+ const submit = (event: SubmitEvent) => {
+ event.preventDefault();
+ const value = term().trim();
+ if (value) enterSearch({ q: value });
+ };
+
+ return (
+
+ Query tracker
+ Search
+
+
+ Active query: {currentQuery().q ?? 'none'}
+
+
+ );
+}
+
+function ProtectedPage() {
+ const authorized = useUnit($authorized);
+
+ return (
+
+ Guarded route
+ Protected workspace
+
+ Authorization: {authorized() ? 'enabled' : 'disabled'}
+
+
+ );
+}
+
+function GuardControls() {
+ const authorized = useUnit($authorized);
+ const changeAuth = useUnit(authChanged);
+ const currentRouter = useRouter();
+
+ return (
+
+ Auth: {authorized() ? 'enabled' : 'disabled'}
+ changeAuth(!authorized())}
+ >
+ Toggle auth
+
+
+ currentRouter.onNavigate({ path: '/protected', query: {} })
+ }
+ >
+ Retry protected
+
+
+ );
+}
+
+function SettingsPage() {
+ return (
+
+ Nested router
+ Settings
+
+ General
+ Profile
+
+
+
+ );
+}
+
+const SettingsRoutes = createRoutesView({
+ routes: [
+ createRouteView({
+ route: settingsGeneralRoute,
+ view: () => General settings
,
+ }),
+ createRouteView({
+ route: settingsProfileRoute,
+ view: () => Profile settings
,
+ }),
+ ],
+ otherwise: () => Choose a setting
,
+});
+
+function HelpPage() {
+ return (
+
+ Help
+ Pathless route mapped to /help.
+
+ );
+}
+
+function NotFoundPage() {
+ return (
+
+ );
+}
+
+function ModalLauncher() {
+ return (
+
+
+ Open task modal
+
+
+ );
+}
+
+function TaskModal() {
+ const params = useUnit(modalTaskRoute.$params);
+ const modal = useRouter();
+ return (
+
+
+ Query router
+ {taskNames[params().taskId] ?? params().taskId}
+ Modal route: {params().taskId}
+ modal.onNavigate({ path: '/', query: {} })}
+ >
+ Close
+
+
+
+ );
+}
+
+const ModalRoutes = createRoutesView({
+ routes: [createRouteView({ route: modalTaskRoute, view: TaskModal })],
+});
+
+const homeView = createRouteView({ route: homeRoute, view: HomePage });
+const searchView = createRouteView({ route: searchRoute, view: SearchPage });
+const projectView = createRouteView({
+ route: projectsRoute,
+ view: ProjectPage,
+ layout: (props) => {props.children}
,
+ children: [
+ createRouteView({ route: projectOverviewRoute, view: ProjectOverviewPage }),
+ createRouteView({ route: projectTaskRoute, view: ProjectTaskPage }),
+ ],
+});
+const reportsView = createLazyRouteView({
+ route: reportsRoute,
+ view: () => import('./pages/ReportsPage'),
+ fallback: () => (
+
+ ),
+ layout: MainLayout,
+});
+const protectedView = createRouteView({
+ route: protectedVisibleRoute,
+ view: ProtectedPage,
+});
+const settingsView = createRouteView({
+ route: settingsRouter,
+ view: SettingsPage,
+});
+const helpView = createRouteView({ route: helpRoute, view: HelpPage });
+
+const mainViews = [
+ ...withLayout(MainLayout, [homeView, searchView, helpView]),
+ projectView,
+ reportsView,
+ protectedView,
+ settingsView,
+];
+
+const MainRoutes = createRoutesView({
+ routes: mainViews,
+ otherwise: NotFoundPage,
+});
+
+function ActiveViews() {
+ const views = useOpenedViews(mainViews);
+ const labels = new Map([
+ [homeRoute, 'home'],
+ [searchRoute, 'search'],
+ [projectsRoute, 'projects'],
+ [reportsRoute, 'reports'],
+ [protectedVisibleRoute, 'protected'],
+ [settingsRouter, 'settings'],
+ [helpRoute, 'help'],
+ ]);
+
+ return (
+
+ Views
+
+ {views()
+ .map((view) => labels.get(view.route as any) ?? 'nested')
+ .join(' > ') || 'none'}
+
+
+ );
+}
+
+function Inspector() {
+ const currentRouter = useRouter();
+ const contextRouter = useRouterContext();
+ const reportsOpened = useIsOpened(reportsRoute);
+ const settingsOpened = useIsOpened(settingsRouter);
+ const projectGroupOpened = useIsOpened(projectGroup as any);
+
+ return (
+
+
+ Router state
+
+ {contextRouter.knownRoutes.length} routes
+
+
+
+ Path
+ {currentRouter.path()}
+
+
+ Query
+ {JSON.stringify(currentRouter.query())}
+
+
+ Reports
+ {reportsOpened() ? 'open' : 'closed'}
+
+
+ Settings
+ {settingsOpened() ? 'open' : 'closed'}
+
+
+ Project group
+ {projectGroupOpened() ? 'open' : 'closed'}
+
+
+
+ currentRouter.onBack()}
+ >
+ Back
+
+ currentRouter.onForward()}
+ >
+ Forward
+
+
+
+ );
+}
+
+function Drawer() {
+ const opened = useUnit(drawerRoute.$isOpened);
+ const drawer = useUnit(drawerRoute);
+ return (
+
+
+ Activity panel: {drawer.params()}
+ drawer.onClose()}
+ >
+ Close
+
+
+
+ );
+}
+
+export function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/solid-router/src/main.tsx b/examples/solid-router/src/main.tsx
new file mode 100644
index 0000000..6ef2d8f
--- /dev/null
+++ b/examples/solid-router/src/main.tsx
@@ -0,0 +1,19 @@
+import { render } from 'solid-js/web';
+import { Provider } from 'effector-solid';
+import { App } from './App';
+import { initializeRouter, scope } from './routing/bootstrap';
+import './styles.css';
+
+const root = document.getElementById('root');
+if (!root) throw new Error('Root element not found');
+
+await initializeRouter();
+
+render(
+ () => (
+
+
+
+ ),
+ root,
+);
diff --git a/examples/solid-router/src/pages/ReportsPage.tsx b/examples/solid-router/src/pages/ReportsPage.tsx
new file mode 100644
index 0000000..6a84967
--- /dev/null
+++ b/examples/solid-router/src/pages/ReportsPage.tsx
@@ -0,0 +1,19 @@
+import { onCleanup, onMount } from 'solid-js';
+
+export default function ReportsPage() {
+ onMount(() => {
+ document.body.dataset.reportsMounted = 'true';
+ });
+
+ onCleanup(() => {
+ delete document.body.dataset.reportsMounted;
+ });
+
+ return (
+
+ Analytics
+ Reports
+ Lazy route content loaded from a separate module.
+
+ );
+}
diff --git a/examples/solid-router/src/routing/bootstrap.ts b/examples/solid-router/src/routing/bootstrap.ts
new file mode 100644
index 0000000..ebd1019
--- /dev/null
+++ b/examples/solid-router/src/routing/bootstrap.ts
@@ -0,0 +1,24 @@
+import { allSettled, fork } from 'effector';
+import { createBrowserHistory } from 'history';
+import { historyAdapter, queryAdapter } from '@effector/router';
+import { modalRouter, router, settingsRouter } from './routes';
+
+export const browserHistory = createBrowserHistory();
+export const scope = fork();
+
+export async function initializeRouter() {
+ await allSettled(router.setHistory, {
+ scope,
+ params: historyAdapter(browserHistory),
+ });
+ await allSettled(settingsRouter.setHistory, {
+ scope,
+ params: historyAdapter(browserHistory),
+ });
+ await allSettled(modalRouter.setHistory, {
+ scope,
+ params: queryAdapter(browserHistory, { key: 'modal' }),
+ });
+
+ await allSettled(scope);
+}
diff --git a/examples/solid-router/src/routing/routes.ts b/examples/solid-router/src/routing/routes.ts
new file mode 100644
index 0000000..a89cb1d
--- /dev/null
+++ b/examples/solid-router/src/routing/routes.ts
@@ -0,0 +1,82 @@
+import { attach, createEvent, createStore } from 'effector';
+import {
+ chainRoute,
+ createRoute,
+ createRouter,
+ createRouterControls,
+ createVirtualRoute,
+ group,
+} from '@effector/router';
+import type { RouteOpenedPayload } from '@effector/router';
+
+export const homeRoute = createRoute({ path: '/' });
+export const projectsRoute = createRoute({ path: '/projects/:projectId' });
+export const projectOverviewRoute = createRoute({
+ path: '/overview',
+ parent: projectsRoute,
+});
+export const projectTaskRoute = createRoute({
+ path: '/tasks/:taskId',
+ parent: projectsRoute,
+});
+export const searchRoute = createRoute({ path: '/search' });
+export const reportsRoute = createRoute({ path: '/reports' });
+export const protectedRoute = createRoute({ path: '/protected' });
+
+export const helpRoute = createRoute();
+
+export const settingsGeneralRoute = createRoute({ path: '/' });
+export const settingsProfileRoute = createRoute({ path: '/profile' });
+export const settingsRouter = createRouter({
+ base: '/settings',
+ routes: [settingsGeneralRoute, settingsProfileRoute],
+});
+
+export const modalTaskRoute = createRoute({ path: '/task/:taskId' });
+export const modalRouter = createRouter({ routes: [modalTaskRoute] });
+
+export const router = createRouter({
+ controls: createRouterControls(),
+ routes: [
+ homeRoute,
+ projectsRoute,
+ projectOverviewRoute,
+ projectTaskRoute,
+ searchRoute,
+ reportsRoute,
+ protectedRoute,
+ settingsRouter,
+ ],
+});
+
+router.registerRoute({ path: '/help', route: helpRoute });
+
+export const authChanged = createEvent();
+export const $authorized = createStore(false).on(
+ authChanged,
+ (_, value) => value,
+);
+
+const checkAuthFx = attach({
+ source: $authorized,
+ effect: (authorized, payload: RouteOpenedPayload) => {
+ if (!authorized) throw new Error('Authorization required');
+ return payload;
+ },
+});
+
+export const protectedVisibleRoute = chainRoute({
+ route: protectedRoute,
+ beforeOpen: checkAuthFx,
+ openOn: checkAuthFx.done,
+ cancelOn: checkAuthFx.fail,
+});
+
+export const projectGroup = group([
+ projectsRoute,
+ projectOverviewRoute,
+ projectTaskRoute,
+]);
+export const drawerRoute = createVirtualRoute<{ panel: string }, string>({
+ transformer: ({ panel }) => panel,
+});
diff --git a/examples/solid-router/src/styles.css b/examples/solid-router/src/styles.css
new file mode 100644
index 0000000..b5cd0bc
--- /dev/null
+++ b/examples/solid-router/src/styles.css
@@ -0,0 +1,58 @@
+:root {
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
+ color: #19212b;
+ background: #eef2f5;
+ font-synthesis: none;
+}
+* { box-sizing: border-box; }
+body { margin: 0; min-width: 320px; }
+button, input { font: inherit; }
+button, a { -webkit-tap-highlight-color: transparent; }
+a { color: #176b87; text-decoration: none; }
+a:hover, a.active { color: #0b4358; text-decoration: underline; }
+button { border: 1px solid #b8c5cc; border-radius: 5px; background: #fff; color: #19212b; padding: 8px 11px; cursor: pointer; }
+button:hover { border-color: #176b87; }
+.shell { min-height: 100vh; }
+.topbar { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 18px 28px; background: #19323d; color: #fff; }
+.brand { color: #fff; font-weight: 700; letter-spacing: .02em; }
+.topnav { display: flex; flex-wrap: wrap; gap: 18px; }
+.topnav a { color: #dcecf0; }
+.topnav a.active { color: #fff; font-weight: 700; }
+.content { max-width: 860px; margin: 0 auto; padding: 44px 28px 100px; }
+.inspector { position: fixed; right: 18px; bottom: 18px; width: 280px; padding: 15px; border: 1px solid #c6d2d8; border-radius: 7px; background: rgba(255, 255, 255, .97); box-shadow: 0 8px 30px rgba(25, 50, 61, .12); font-size: 13px; }
+.inspector-heading, .inspector-row, .inspector-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
+.inspector-heading { padding-bottom: 10px; border-bottom: 1px solid #e1e7ea; font-weight: 700; }
+.inspector-row { padding-top: 9px; }
+.inspector-row strong { max-width: 175px; overflow-wrap: anywhere; text-align: right; font-weight: 600; }
+.inspector-actions { justify-content: flex-start; padding-top: 12px; }
+.status-chip, .card-label, .eyebrow { color: #607581; font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
+.status-chip { padding: 3px 6px; border: 1px solid #c6d2d8; border-radius: 4px; letter-spacing: .02em; }
+h1, h2, p { margin-top: 0; }
+h1 { margin-bottom: 10px; font-size: 38px; line-height: 1.1; }
+h2 { margin-bottom: 8px; font-size: 22px; }
+.lede { color: #607581; font-size: 17px; }
+.project-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin-top: 28px; }
+.project-card { padding: 18px; border: 1px solid #c6d2d8; border-radius: 7px; background: #fff; }
+.project-card p { color: #607581; }
+.project-card a { display: inline-block; margin-top: 10px; }
+.action-row, .tabs { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 24px; }
+.tabs { border-bottom: 1px solid #c6d2d8; padding-bottom: 10px; }
+.tabs a { padding: 5px 0; }
+.section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
+.outlet, .project-layout { min-height: 120px; }
+.outlet { padding-top: 25px; }
+.search-form { display: flex; flex-wrap: wrap; gap: 8px; margin: 28px 0 16px; }
+.search-form input { min-width: 240px; border: 1px solid #b8c5cc; border-radius: 5px; padding: 8px 10px; }
+.muted { display: block; margin-top: 14px; color: #607581; font-family: ui-monospace, monospace; font-size: 12px; }
+ .drawer { position: fixed; left: 18px; bottom: 78px; display: flex; gap: 12px; align-items: center; padding: 14px; border: 1px solid #9bb7c2; border-radius: 6px; background: #e5f2f5; }
+.guard-controls { position: fixed; left: 18px; bottom: 18px; z-index: 3; display: flex; gap: 10px; align-items: center; padding: 10px 12px; border: 1px solid #c6d2d8; border-radius: 6px; background: #fff; font-size: 12px; }
+.modal-launcher { position: fixed; left: 18px; top: 78px; z-index: 2; }
+.modal-backdrop { position: fixed; inset: 0; z-index: 5; display: grid; place-items: center; background: rgba(25, 50, 61, .35); }
+.modal { width: min(420px, calc(100vw - 32px)); padding: 24px; border-radius: 7px; background: #fff; box-shadow: 0 18px 50px rgba(25, 50, 61, .25); }
+@media (max-width: 760px) {
+ .topbar { align-items: flex-start; flex-direction: column; padding: 16px 18px; }
+ .content { padding: 32px 18px 180px; }
+ .project-grid { grid-template-columns: 1fr; }
+ .inspector { right: 12px; bottom: 12px; left: 12px; width: auto; }
+ .modal-launcher { left: 18px; top: 128px; }
+}
diff --git a/examples/solid-router/tsconfig.json b/examples/solid-router/tsconfig.json
new file mode 100644
index 0000000..15e2842
--- /dev/null
+++ b/examples/solid-router/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "declaration": false,
+ "types": ["vite/client", "@playwright/test"]
+ },
+ "include": ["src", "e2e", "vite.config.ts", "playwright.config.ts"],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/examples/solid-router/vite.config.ts b/examples/solid-router/vite.config.ts
new file mode 100644
index 0000000..a9bc189
--- /dev/null
+++ b/examples/solid-router/vite.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite';
+import solid from 'vite-plugin-solid';
+
+export default defineConfig({
+ plugins: [solid({ hot: false })],
+ server: { port: 4174, strictPort: true },
+ preview: { port: 4174, strictPort: true },
+});
diff --git a/package.json b/package.json
index a20ee76..3ec0073 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,8 @@
":docs": "pnpm --filter @effector/router-docs",
":react": "pnpm --filter @effector/router-react",
":react-native": "pnpm --filter @effector/router-react-native",
+ ":solid": "pnpm --filter @effector/router-solid",
+ ":solid-example": "pnpm --filter @effector/router-solid-example",
":paths": "pnpm --filter @effector/router-paths",
":core": "pnpm --filter @effector/router"
},
@@ -28,6 +30,7 @@
"@react-navigation/bottom-tabs": "7.15.5",
"@react-navigation/native": "7.1.33",
"@react-navigation/stack": "7.8.4",
+ "@solidjs/testing-library": "^0.8.10",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
@@ -36,6 +39,7 @@
"@vitejs/plugin-react": "^4.3.4",
"effector": "^23.4.4",
"effector-react": "^23.3.0",
+ "effector-solid": "^0.23.0",
"eslint": "10.0.2",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-prettier": "5.5.5",
@@ -49,10 +53,13 @@
"react": "19.2.4",
"react-dom": "19.2.4",
"react-native": "0.84.1",
+ "solid-js": "^1.9.14",
+ "typescript": "5.8.2",
"typescript-eslint": "^8.56.1",
"vite": "7.3.2",
"vite-plugin-babel": "1.5.1",
"vite-plugin-dts": "4.5.4",
+ "vite-plugin-solid": "^2.11.12",
"vitepress": "1.6.4",
"vitest": "4.1.9",
"zod": "^4.3.6"
diff --git a/packages/solid/README.md b/packages/solid/README.md
new file mode 100644
index 0000000..207e424
--- /dev/null
+++ b/packages/solid/README.md
@@ -0,0 +1,89 @@
+# ☄️ @effector/router-solid
+
+[](https://www.npmjs.com/package/@effector/router-solid)
+
+> ⚠️ **Draft.** API mirrors [`@effector/router-react`](https://www.npmjs.com/package/@effector/router-react) and may change before the first stable release.
+
+SolidJS bindings for [`@effector/router`](https://www.npmjs.com/package/@effector/router). Connect
+routes to components, render the active route, and navigate with a ` ` — all driven by
+Effector state.
+
+Only the current major of SolidJS (**1.x**) is supported.
+
+## Install
+
+```bash
+npm install @effector/router-solid @effector/router effector effector-solid solid-js
+```
+
+## Quick start
+
+```tsx
+import { createRoute, createRouter } from '@effector/router';
+import {
+ RouterProvider,
+ createRouteView,
+ createRoutesView,
+ Link,
+} from '@effector/router-solid';
+
+// 1. Routes + router (see @effector/router for history setup)
+const home = createRoute({ path: '/' });
+const about = createRoute({ path: '/about' });
+const router = createRouter({ routes: [home, about] });
+
+// 2. Bind each route to a component
+const HomeScreen = createRouteView({
+ route: home,
+ view: () => Go to About,
+});
+const AboutScreen = createRouteView({
+ route: about,
+ view: () => Back Home,
+});
+
+// 3. Render the active route
+const Routes = createRoutesView({ routes: [HomeScreen, AboutScreen] });
+
+export function App() {
+ return (
+
+
+
+ );
+}
+```
+
+Read route params in a view with effector-solid's `useUnit` (returns a Solid accessor):
+
+```tsx
+import { useUnit } from 'effector-solid';
+
+const UserScreen = createRouteView({
+ route: userRoute, // createRoute({ path: '/user/:id' })
+ view: () => {
+ const params = useUnit(userRoute.$params);
+ return User {params().id}
;
+ },
+});
+```
+
+## API
+
+- `RouterProvider` — provide the router to the tree.
+- `createRouteView` / `createLazyRouteView` — bind a route to a component (with optional lazy loading).
+- `createRoutesView` — render the currently opened route, with an `otherwise` fallback.
+- `Link`, `useLink` — declarative and imperative navigation.
+- `withLayout` — share a layout across routes.
+- `Outlet`, `useRouter`, `useIsOpened`, `useOpenedViews` — composition helpers.
+
+Unlike the React binding, reactive helpers (`useLink`, `useIsOpened`, `useOpenedViews`) return Solid
+**accessors** (`() => value`) instead of plain values.
+
+## Documentation
+
+Full guides and API reference: **[router.effector.dev/solid](https://router.effector.dev/solid)**
+
+## License
+
+[MIT](https://github.com/effector/router/blob/main/LICENSE)
diff --git a/packages/solid/lib/context.ts b/packages/solid/lib/context.ts
new file mode 100644
index 0000000..962b75d
--- /dev/null
+++ b/packages/solid/lib/context.ts
@@ -0,0 +1,8 @@
+import type { Router } from '@effector/router';
+import { createContext } from 'solid-js';
+import type { RouteView } from './types';
+
+export const RouterProviderContext = createContext(null);
+export const OutletContext = createContext<{ children: RouteView[] }>({
+ children: [],
+});
diff --git a/packages/solid/lib/create-lazy-route-view.tsx b/packages/solid/lib/create-lazy-route-view.tsx
new file mode 100644
index 0000000..4eabd84
--- /dev/null
+++ b/packages/solid/lib/create-lazy-route-view.tsx
@@ -0,0 +1,53 @@
+import { lazy, Suspense } from 'solid-js';
+import { is } from '@effector/router';
+import type { InternalRoute } from '@effector/router';
+import type { CreateLazyRouteViewProps, RouteView } from './types';
+
+/**
+ * @description Creates Lazy route view with async bundle load
+ * @link https://router.effector.dev/solid/create-lazy-route-view.html
+ * @param props Lazy route view props
+ * @returns RouteView
+ * @example ```ts
+ * // profile.tsx
+ * export default function () {
+ * return <>...>;
+ * }
+ *
+ * // index.ts
+ * import { createLazyRouteView } from '@effector/router-solid';
+ * import { routes } from '@shared/routing';
+ * import { MainLayout } from '@layouts';
+ *
+ * export const ProfileScreen = createLazyRouteView({
+ * route: routes.profile,
+ * view: () => import('./profile'),
+ * fallback: () => ':(',
+ * layout: MainLayout,
+ * });
+ * ```
+ */
+export function createLazyRouteView(
+ props: CreateLazyRouteViewProps,
+): RouteView {
+ if (!is.router(props.route)) {
+ (props.route as InternalRoute).internal.setAsyncImport(props.view);
+ }
+
+ const View = lazy(props.view);
+ const { layout: Layout, fallback: Fallback, children } = props;
+
+ const inner = () => (
+ : null}>
+
+
+ );
+
+ const view = Layout ? () => {inner()} : inner;
+
+ return {
+ route: props.route,
+ view,
+ children,
+ };
+}
diff --git a/packages/solid/lib/create-route-view.tsx b/packages/solid/lib/create-route-view.tsx
new file mode 100644
index 0000000..056acf9
--- /dev/null
+++ b/packages/solid/lib/create-route-view.tsx
@@ -0,0 +1,42 @@
+import type { CreateRouteViewProps, RouteView } from './types';
+
+/**
+ * @description Creates Route view without async bundle load
+ * @link https://router.effector.dev/solid/create-route-view.html
+ * @param props Route view props
+ * @returns RouteView
+ * @example ```ts
+ * import { createRouteView } from '@effector/router-solid';
+ * import { routes } from '@shared/routing';
+ * import { MainLayout } from '@layouts';
+ *
+ * function Profile() {
+ * return <>...>;
+ * }
+ *
+ * export const ProfileScreen = createRouteView({
+ * route: routes.profile,
+ * view: Profile,
+ * layout: MainLayout,
+ * });
+ * ```
+ */
+export function createRouteView(
+ props: CreateRouteViewProps,
+): RouteView {
+ const { layout: Layout, view: View, children } = props;
+
+ const view = Layout
+ ? () => (
+
+
+
+ )
+ : () => ;
+
+ return {
+ route: props.route,
+ view,
+ children,
+ };
+}
diff --git a/packages/solid/lib/create-routes-view.tsx b/packages/solid/lib/create-routes-view.tsx
new file mode 100644
index 0000000..d6394c2
--- /dev/null
+++ b/packages/solid/lib/create-routes-view.tsx
@@ -0,0 +1,52 @@
+import { Show, type Component } from 'solid-js';
+import { Dynamic } from 'solid-js/web';
+import { OutletContext } from './context';
+import { useOpenedViews } from './use-opened-views';
+import type { RouteView } from './types';
+
+interface CreateRoutesViewProps {
+ routes: RouteView[];
+ otherwise?: Component;
+}
+
+/**
+ * @description Create routes view which renders current opened route. `Don't forget add `!
+ * @param props Routes view config
+ * @link https://router.effector.dev/solid/create-routes-view.html
+ * @returns RoutesView
+ * @example ```tsx
+ * import { createRoutesView } from '@effector/router-solid';
+ * import { router } from './router';
+ * // feed screen & profile screen must be created with createRouteView!
+ * import { FeedScreen, ProfileScreen } from './screens';
+ *
+ * const RoutesView = createRoutesView({ routes: [FeedScreen, ProfileScreen] });
+ *
+ * // then you can use it like a Solid component:
+ * function App() {
+ * return (
+ *
+ *
+ *
+ * );
+ * }
+ * ```
+ */
+export function createRoutesView(props: CreateRoutesViewProps) {
+ const { routes, otherwise: NotFound } = props;
+
+ return () => {
+ const openedViews = useOpenedViews(routes);
+ const openedView = () => openedViews().at(-1);
+
+ return (
+ : null} keyed>
+ {(view) => (
+
+
+
+ )}
+
+ );
+ };
+}
diff --git a/packages/solid/lib/index.ts b/packages/solid/lib/index.ts
new file mode 100644
index 0000000..7500cc0
--- /dev/null
+++ b/packages/solid/lib/index.ts
@@ -0,0 +1,17 @@
+export { RouterProvider } from './router-provider';
+export { Link } from './link';
+export { createRouteView } from './create-route-view';
+export { createRoutesView } from './create-routes-view';
+export { createLazyRouteView } from './create-lazy-route-view';
+export { useRouter, useRouterContext } from './use-router';
+export { withLayout } from './with-layout';
+export type {
+ LinkProps,
+ CreateLazyRouteViewProps,
+ CreateRouteViewProps,
+ RouteView,
+} from './types';
+export { Outlet } from './outlet';
+export { useOpenedViews } from './use-opened-views';
+export { useIsOpened } from './use-is-opened';
+export { useLink } from './use-link';
diff --git a/packages/solid/lib/link.tsx b/packages/solid/lib/link.tsx
new file mode 100644
index 0000000..2323c2d
--- /dev/null
+++ b/packages/solid/lib/link.tsx
@@ -0,0 +1,98 @@
+import type { Route, RouteOpenedPayload } from '@effector/router';
+import { splitProps, type JSX } from 'solid-js';
+import type { LinkProps } from './types';
+import { useLink } from './use-link';
+
+/**
+ * @description Navigates user to provided route on click
+ * @link https://router.effector.dev/solid/link.html
+ * @example ```tsx
+ * import { Link } from '@effector/router-solid';
+ * import { routes } from '@shared/routing';
+ *
+ * function Profile(props) {
+ * return (
+ * <>
+ * Settings
+ *
+ *
+ * {(post) => (
+ *
+ * Edit post
+ *
+ * )}
+ *
+ * >
+ * );
+ * }
+ * ```
+ */
+export function Link(
+ props: LinkProps,
+) {
+ const [local, anchorProps] = splitProps(props, [
+ 'to',
+ 'params',
+ 'onClick',
+ 'replace',
+ 'query',
+ 'children',
+ ]);
+
+ const { path, onOpen } = useLink(
+ local.to as Route,
+ () => local.params as Params,
+ );
+
+ const handleClick: JSX.EventHandler = (
+ event,
+ ) => {
+ callClickHandler(local.onClick, event);
+
+ // allow user to prevent navigation
+ if (event.defaultPrevented) {
+ return;
+ }
+
+ // let browser handle "_blank" target and etc
+ if (anchorProps.target && anchorProps.target !== '_self') {
+ return;
+ }
+
+ // skip modified events (like cmd + click to open the link in new tab)
+ if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
+ return;
+ }
+
+ event.preventDefault();
+
+ onOpen({
+ params: local.params || {},
+ replace: local.replace,
+ query: local.query,
+ } as RouteOpenedPayload);
+ };
+
+ return (
+
+ {local.children}
+
+ );
+}
+
+// Solid event handlers can be a plain function or a bound `[handler, data]`
+// tuple. Support both so consumers can pass either form.
+function callClickHandler(
+ handler: JSX.EventHandlerUnion | undefined,
+ event: MouseEvent & { currentTarget: HTMLAnchorElement; target: Element },
+) {
+ if (!handler) {
+ return;
+ }
+
+ if (typeof handler === 'function') {
+ handler(event);
+ } else {
+ handler[0](handler[1], event);
+ }
+}
diff --git a/packages/solid/lib/outlet.tsx b/packages/solid/lib/outlet.tsx
new file mode 100644
index 0000000..83703a8
--- /dev/null
+++ b/packages/solid/lib/outlet.tsx
@@ -0,0 +1,45 @@
+import { Show, useContext } from 'solid-js';
+import { Dynamic } from 'solid-js/web';
+import { OutletContext } from './context';
+import { useOpenedViews } from './use-opened-views';
+
+/**
+ * @description Outlet component for nested routes
+ * @link https://router.effector.dev/solid/outlet.html
+ * @example ```tsx
+ * export const RoutesView = createRoutesView({
+ * routes: [
+ * createRouteView({
+ * route: routes.profile,
+ * view: ProfileScreen,
+ * children: [
+ * createRouteView({ route: routes.settings, view: SettingsScreen }),
+ * ],
+ * }),
+ * ],
+ * });
+ *
+ * // profile.tsx
+ * export const ProfileScreen = () => {
+ * // will render settings screen when profile route is opened
+ * // and settings route is active
+ * return (
+ * <>
+ * Profile
+ *
+ * >
+ * );
+ * };
+ * ```
+ */
+export function Outlet() {
+ const { children } = useContext(OutletContext);
+ const openedViews = useOpenedViews(children);
+ const openedView = () => openedViews().at(-1);
+
+ return (
+
+ {(view) => }
+
+ );
+}
diff --git a/packages/solid/lib/router-provider.tsx b/packages/solid/lib/router-provider.tsx
new file mode 100644
index 0000000..b670661
--- /dev/null
+++ b/packages/solid/lib/router-provider.tsx
@@ -0,0 +1,20 @@
+import type { JSX } from 'solid-js';
+import type { Router } from '@effector/router';
+import { RouterProviderContext } from './context';
+
+interface RouterProviderProps {
+ children?: JSX.Element;
+ router: Router;
+}
+
+/**
+ * @description Provides router in Solid tree
+ * @param props Router provider config
+ */
+export function RouterProvider(props: RouterProviderProps) {
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/packages/solid/lib/types.ts b/packages/solid/lib/types.ts
new file mode 100644
index 0000000..d7035fc
--- /dev/null
+++ b/packages/solid/lib/types.ts
@@ -0,0 +1,45 @@
+import type { Route, OpenPayloadBase, Router } from '@effector/router';
+import type { Component, JSX } from 'solid-js';
+
+type LayoutComponent = Component<{ children: JSX.Element }>;
+type RouteViewWithLayout = RouteView & { layout?: LayoutComponent };
+
+interface CreateBaseRouteViewProps {
+ route: Route | Router;
+ layout?: LayoutComponent;
+ children?: RouteViewWithLayout[];
+}
+
+export interface CreateRouteViewProps<
+ T extends object | void = void,
+> extends CreateBaseRouteViewProps {
+ view: Component;
+}
+
+export interface CreateLazyRouteViewProps<
+ T extends object | void = void,
+> extends CreateBaseRouteViewProps {
+ view: () => Promise<{ default: Component }>;
+ fallback?: Component;
+}
+
+export interface RouteView {
+ route: Route | Router;
+ view: Component;
+ children?: RouteView[];
+}
+
+type AnchorProps = Omit, 'href'>;
+
+type BaseLinkProps = {
+ to: Route;
+ children?: JSX.Element;
+} & AnchorProps &
+ OpenPayloadBase;
+
+export type LinkProps = Params extends
+ | Record
+ | void
+ | undefined
+ ? BaseLinkProps & { params?: Params }
+ : BaseLinkProps & { params: Params };
diff --git a/packages/solid/lib/use-is-opened.ts b/packages/solid/lib/use-is-opened.ts
new file mode 100644
index 0000000..4707c37
--- /dev/null
+++ b/packages/solid/lib/use-is-opened.ts
@@ -0,0 +1,16 @@
+import { is, type Route, type Router } from '@effector/router';
+import { useUnit } from 'effector-solid';
+import { createMemo, type Accessor } from 'solid-js';
+
+/**
+ * @description Reactive accessor telling whether a route (or any route of a
+ * router) is currently opened.
+ */
+export function useIsOpened(route: Route | Router): Accessor {
+ if (is.router(route)) {
+ const activeRoutes = useUnit(route.$activeRoutes);
+ return createMemo(() => activeRoutes().length > 0);
+ }
+
+ return useUnit(route.$isOpened);
+}
diff --git a/packages/solid/lib/use-link.ts b/packages/solid/lib/use-link.ts
new file mode 100644
index 0000000..b8cd67d
--- /dev/null
+++ b/packages/solid/lib/use-link.ts
@@ -0,0 +1,36 @@
+import type { Route, InternalRoute } from '@effector/router';
+import { createMemo, type Accessor } from 'solid-js';
+import { useRouterContext } from './use-router';
+import { useUnit } from 'effector-solid';
+
+/**
+ * @description Imperative navigation helper. Resolves a route to a reactive
+ * `path` accessor and an `onOpen` event you can call to navigate.
+ * @param to Target route (must be registered in the router)
+ * @param params Accessor to the route params (reactive)
+ */
+export function useLink(
+ to: Route,
+ params: Accessor = (() => undefined) as Accessor,
+) {
+ const { knownRoutes } = useRouterContext();
+ const target = knownRoutes.find(
+ ({ route }) => route === (to as unknown as InternalRoute),
+ );
+
+ if (!target) {
+ console.error(`[useLink route log]`, to);
+ throw new Error(
+ `[useLink] Route not found. Maybe it is not passed into createRouter?`,
+ );
+ }
+
+ const { onOpen } = useUnit(to);
+
+ const path = createMemo(() => target.build(params() ?? undefined));
+
+ return {
+ path,
+ onOpen,
+ };
+}
diff --git a/packages/solid/lib/use-opened-views.ts b/packages/solid/lib/use-opened-views.ts
new file mode 100644
index 0000000..fd5c630
--- /dev/null
+++ b/packages/solid/lib/use-opened-views.ts
@@ -0,0 +1,33 @@
+import { createMemo, type Accessor } from 'solid-js';
+import type { RouteView } from './types';
+import type { InternalRoute } from '@effector/router';
+import { is } from '@effector/router';
+import { useUnit } from 'effector-solid';
+
+/**
+ * @description Reactive accessor with the currently opened views out of the
+ * provided list. Parent views are filtered out when a child view is opened, so
+ * only the deepest matching branch remains.
+ */
+export function useOpenedViews(routes: RouteView[]): Accessor {
+ const visibilities = routes.map>((view) => {
+ if (is.router(view.route)) {
+ const activeRoutes = useUnit(view.route.$activeRoutes);
+ return () => activeRoutes().length > 0;
+ }
+
+ return useUnit(view.route.$isOpened);
+ });
+
+ return createMemo(() => {
+ const filtered = routes.filter((_, index) => visibilities[index]());
+
+ return filtered.reduce(
+ (acc, view) =>
+ acc.filter(
+ (r) => r.route !== (view.route as InternalRoute).parent,
+ ),
+ filtered,
+ );
+ });
+}
diff --git a/packages/solid/lib/use-router.ts b/packages/solid/lib/use-router.ts
new file mode 100644
index 0000000..490c028
--- /dev/null
+++ b/packages/solid/lib/use-router.ts
@@ -0,0 +1,24 @@
+import { useContext } from 'solid-js';
+import { RouterProviderContext } from './context';
+import { useUnit } from 'effector-solid';
+
+export function useRouterContext() {
+ const context = useContext(RouterProviderContext);
+
+ if (!context) {
+ throw new Error(
+ '[useRouter] Router not found. Add RouterProvider in app root',
+ );
+ }
+
+ return context;
+}
+
+/**
+ * @description Use router from provider
+ * @returns Router unit shape (Solid accessors)
+ * @link https://router.effector.dev/solid/use-router.html
+ */
+export function useRouter() {
+ return useUnit(useRouterContext());
+}
diff --git a/packages/solid/lib/with-layout.tsx b/packages/solid/lib/with-layout.tsx
new file mode 100644
index 0000000..2dabc7f
--- /dev/null
+++ b/packages/solid/lib/with-layout.tsx
@@ -0,0 +1,47 @@
+import type { Component, JSX } from 'solid-js';
+import type { RouteView } from './types';
+
+/**
+ * @description Group routes by layout, so you don't need to pass `layout` property manually in all routes. Works for `createRouteView` and `createLazyRouteView`.
+ * @link https://router.effector.dev/solid/with-layout.html
+ * @example ```tsx
+ * import {
+ * createRoutesView,
+ * createRouteView,
+ * withLayout,
+ * } from '@effector/router-solid';
+ *
+ * import { SignInScreen } from './sign-in';
+ * import { SignUpScreen } from './sign-up';
+ * import { ProfileScreen } from './profile';
+ *
+ * import { routes } from '@shared/routing';
+ *
+ * import { AuthLayout } from '@layouts/auth';
+ *
+ * export const RoutesView = createRoutesView({
+ * routes: [
+ * ...withLayout(AuthLayout, [
+ * createRouteView({ route: routes.signIn, view: SignInScreen }),
+ * createRouteView({ route: routes.signUp, view: SignUpScreen }),
+ * ]),
+ * createRouteView({ route: routes.profile, view: ProfileScreen }),
+ * ],
+ * });
+ * ```
+ */
+export function withLayout(
+ layout: Component<{ children: JSX.Element }>,
+ views: RouteView[],
+): RouteView[] {
+ const Layout = layout;
+
+ return views.map(({ route, view: View }) => ({
+ route,
+ view: () => (
+
+
+
+ ),
+ }));
+}
diff --git a/packages/solid/package.json b/packages/solid/package.json
new file mode 100644
index 0000000..74bc401
--- /dev/null
+++ b/packages/solid/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "@effector/router-solid",
+ "version": "0.1.0",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org/"
+ },
+ "private": false,
+ "description": "SolidJS bindings for effector router",
+ "keywords": [
+ "effector",
+ "argon",
+ "atomic",
+ "router",
+ "effector/router",
+ "solid",
+ "solid-js"
+ ],
+ "contributors": [
+ "Sergey Sova ",
+ "Anton Kosykh",
+ "Zero Bias ",
+ "movpushmov"
+ ],
+ "homepage": "https://router.effector.dev/solid",
+ "license": "MIT",
+ "type": "module",
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "require": "./dist/index.cjs"
+ }
+ },
+ "directories": {
+ "lib": "lib"
+ },
+ "files": [
+ "dist"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/effector/router.git",
+ "directory": "packages/solid"
+ },
+ "scripts": {
+ "build": "vite build",
+ "test": "vitest run"
+ },
+ "bugs": {
+ "url": "https://github.com/effector/router/issues"
+ },
+ "dependencies": {
+ "@effector/router": "workspace:^"
+ },
+ "peerDependencies": {
+ "effector": ">=23",
+ "effector-solid": ">=0.23",
+ "solid-js": "^1.9.0"
+ }
+}
diff --git a/packages/solid/tests/example-app.test.tsx b/packages/solid/tests/example-app.test.tsx
new file mode 100644
index 0000000..d7be3ff
--- /dev/null
+++ b/packages/solid/tests/example-app.test.tsx
@@ -0,0 +1,178 @@
+import { allSettled, createEffect, fork } from 'effector';
+import { Provider, useUnit } from 'effector-solid';
+import { describe, expect, test } from 'vitest';
+import { render, screen } from '@solidjs/testing-library';
+import {
+ chainRoute,
+ createRoute,
+ createRouter,
+ historyAdapter,
+ type RouteOpenedPayload,
+} from '@effector/router';
+import { createMemoryHistory } from 'history';
+
+import {
+ createRouteView,
+ createRoutesView,
+ Link,
+ RouterProvider,
+} from '../lib';
+
+// --- "database" -----------------------------------------------------------
+const posts: Record = {
+ '1': 'Hello world',
+ '2': 'Effector rules',
+};
+
+// --- routes ---------------------------------------------------------------
+const homeRoute = createRoute({ path: '/' });
+const postRoute = createRoute({ path: '/post/:id' });
+
+// Guard the post route: only open it when the requested post exists.
+// When the effect rejects, `cancelOn` keeps the virtual route closed, so the
+// routes view falls through to the `otherwise` (404) fallback.
+const loadPostFx = createEffect(
+ ({ params }: RouteOpenedPayload<{ id: string }>) => {
+ const title = posts[params.id];
+ if (!title) {
+ throw new Error(`Post "${params.id}" not found`);
+ }
+ return title;
+ },
+);
+
+const postVisible = chainRoute({
+ route: postRoute,
+ beforeOpen: loadPostFx,
+ openOn: loadPostFx.done,
+ cancelOn: loadPostFx.fail,
+});
+
+// --- pages ----------------------------------------------------------------
+function HomePage() {
+ return (
+
+
Home
+
+ Read first post
+
+
+ );
+}
+
+function PostPage() {
+ const params = useUnit(postVisible.$params);
+ return (
+
+
Post
+
{posts[params().id]}
+
+ );
+}
+
+function NotFoundPage() {
+ return (
+
+
404 — Not Found
+
+ );
+}
+
+const RoutesView = createRoutesView({
+ routes: [
+ createRouteView({ route: homeRoute, view: HomePage }),
+ createRouteView({ route: postVisible, view: PostPage }),
+ ],
+ otherwise: NotFoundPage,
+});
+
+function setup(initialPath: string) {
+ const scope = fork();
+ const router = createRouter({ routes: [homeRoute, postRoute] });
+ const history = createMemoryHistory({ initialEntries: [initialPath] });
+
+ render(() => (
+
+
+
+
+
+ ));
+
+ return { scope, router, history };
+}
+
+describe('example app routing', () => {
+ test('renders the home page on "/"', async () => {
+ const { scope, router, history } = setup('/');
+
+ await allSettled(router.setHistory, {
+ scope,
+ params: historyAdapter(history),
+ });
+
+ expect(screen.getByTestId('page').textContent).toContain('Home');
+ });
+
+ test('renders the post page for an existing post', async () => {
+ const { scope, router, history } = setup('/');
+
+ await allSettled(router.setHistory, {
+ scope,
+ params: historyAdapter(history),
+ });
+
+ history.push('/post/2');
+ await allSettled(scope);
+
+ expect(screen.getByTestId('page').textContent).toContain('Post');
+ expect(screen.getByTestId('post-title').textContent).toBe('Effector rules');
+ });
+
+ test('shows 404 when the post does not exist', async () => {
+ const { scope, router, history } = setup('/');
+
+ await allSettled(router.setHistory, {
+ scope,
+ params: historyAdapter(history),
+ });
+
+ // URL matches /post/:id, but there is no post with this id
+ history.push('/post/999');
+ await allSettled(scope);
+
+ expect(screen.getByTestId('page').textContent).toContain('404');
+ });
+
+ test('shows 404 for an unknown / malformed url', async () => {
+ const { scope, router, history } = setup('/');
+
+ await allSettled(router.setHistory, {
+ scope,
+ params: historyAdapter(history),
+ });
+
+ // No route matches this path at all
+ history.push('/this/does/not/exist');
+ await allSettled(scope);
+
+ expect(screen.getByTestId('page').textContent).toContain('404');
+ });
+
+ test('recovers from 404 back to a real page', async () => {
+ const { scope, router, history } = setup('/');
+
+ await allSettled(router.setHistory, {
+ scope,
+ params: historyAdapter(history),
+ });
+
+ history.push('/nope');
+ await allSettled(scope);
+ expect(screen.getByTestId('page').textContent).toContain('404');
+
+ history.push('/post/1');
+ await allSettled(scope);
+ expect(screen.getByTestId('post-title').textContent).toBe('Hello world');
+ });
+});
diff --git a/packages/solid/tests/index.test.tsx b/packages/solid/tests/index.test.tsx
new file mode 100644
index 0000000..d55f2e4
--- /dev/null
+++ b/packages/solid/tests/index.test.tsx
@@ -0,0 +1,99 @@
+import { allSettled, fork } from 'effector';
+import { Provider } from 'effector-solid';
+import { describe, expect, test } from 'vitest';
+import { render } from '@solidjs/testing-library';
+import { createRoute, createRouter, historyAdapter } from '@effector/router';
+import { createMemoryHistory } from 'history';
+
+import {
+ createLazyRouteView,
+ createRouteView,
+ createRoutesView,
+ Link,
+ RouterProvider,
+} from '../lib';
+
+describe('solid bindings', () => {
+ test('component changes when path changes', async () => {
+ const route1 = createRoute({ path: '/app' });
+ const route2 = createRoute({ path: '/faq' });
+
+ const scope = fork();
+ const router = createRouter({ routes: [route1, route2] });
+
+ const history = createMemoryHistory();
+
+ await allSettled(router.setHistory, {
+ scope,
+ params: historyAdapter(history),
+ });
+
+ const RoutesView = createRoutesView({
+ routes: [
+ { route: route1, view: () => route1
},
+ { route: route2, view: () => route2
},
+ ],
+ otherwise: () => not found
,
+ });
+
+ const { container } = render(() => (
+
+
+
+
+
+ ));
+
+ await allSettled(route1.open, { scope, params: undefined });
+ expect(container.querySelector('#message')?.textContent).toBe('route1');
+
+ await allSettled(route2.open, { scope, params: undefined });
+ expect(container.querySelector('#message')?.textContent).toBe('route2');
+
+ history.push('/not-found');
+ await allSettled(scope);
+ expect(container.querySelector('#message')?.textContent).toBe('not found');
+ });
+
+ test('link renders resolved href', async () => {
+ const route1 = createRoute({ path: '/app' });
+ const route2 = createRoute({ path: '/faq/:id' });
+
+ const scope = fork();
+ const router = createRouter({ routes: [route1, route2] });
+
+ await allSettled(router.setHistory, {
+ scope,
+ params: historyAdapter(createMemoryHistory()),
+ });
+
+ const { container } = render(() => (
+
+
+
+ open
+
+
+
+ ));
+
+ expect(container.querySelector('a')?.getAttribute('href')).toBe('/faq/42');
+ });
+
+ test('lazy route view preserves nested route views', () => {
+ const parentRoute = createRoute({ path: '/parent' });
+ const childRoute = createRoute({ path: '/child', parent: parentRoute });
+ const childView = createRouteView({
+ route: childRoute,
+ view: () => child
,
+ });
+
+ const lazyView = createLazyRouteView({
+ route: parentRoute,
+ view: () => Promise.resolve({ default: () => parent
}),
+ children: [childView],
+ });
+
+ expect(lazyView.children).toEqual([childView]);
+ });
+});
diff --git a/packages/solid/tsconfig.json b/packages/solid/tsconfig.json
new file mode 100644
index 0000000..a46fc79
--- /dev/null
+++ b/packages/solid/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "types": ["vite/client", "vitest/globals"]
+ },
+ "include": ["lib", "tests", "vite.config.ts"],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/packages/solid/vite.config.ts b/packages/solid/vite.config.ts
new file mode 100644
index 0000000..b6b8914
--- /dev/null
+++ b/packages/solid/vite.config.ts
@@ -0,0 +1,67 @@
+import { defineConfig } from 'vitest/config';
+import { resolve } from 'path';
+import dts from 'vite-plugin-dts';
+import solid from 'vite-plugin-solid';
+
+export default defineConfig({
+ mode: 'production',
+ build: {
+ lib: {
+ entry: resolve(__dirname, 'lib/index.ts'),
+ fileName: 'index',
+ formats: ['es', 'cjs'],
+ },
+ rollupOptions: {
+ external: [
+ 'effector',
+ 'effector-solid',
+ '@effector/router',
+ 'solid-js',
+ 'solid-js/web',
+ 'solid-js/store',
+ ],
+ output: {
+ globals: {
+ 'solid-js': 'solid-js',
+ 'solid-js/web': 'solid-js/web',
+ 'solid-js/store': 'solid-js/store',
+ effector: 'effector',
+ 'effector-solid': 'effector-solid',
+ '@effector/router': '@effector/router',
+ },
+ },
+ },
+ },
+ plugins: [
+ // `hot` (solid-refresh) is only useful behind a dev server; this package is
+ // consumed as a library and built/tested only, and the refresh runtime
+ // breaks under vitest, so keep it disabled.
+ solid({ hot: false }),
+ dts({
+ outDir: resolve(__dirname, 'dist'),
+ entryRoot: resolve(__dirname, 'lib'),
+ exclude: [
+ resolve(__dirname, 'tests'),
+ resolve(__dirname, '../core'),
+ resolve(__dirname, '../paths'),
+ resolve(__dirname, '../react'),
+ resolve(__dirname, '../react-native'),
+ ],
+ staticImport: true,
+ insertTypesEntry: true,
+ rollupTypes: true,
+ }),
+ ],
+ resolve: {
+ conditions: ['development', 'browser'],
+ },
+ test: {
+ globals: true,
+ environment: 'happy-dom',
+ server: {
+ deps: {
+ inline: [/solid-js/, /@solidjs\/testing-library/, /effector-solid/],
+ },
+ },
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d516a35..0cd4c19 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,6 +41,9 @@ importers:
'@react-navigation/stack':
specifier: 7.8.4
version: 7.8.4(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-gesture-handler@2.30.0(react-native@0.84.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
+ '@solidjs/testing-library':
+ specifier: ^0.8.10
+ version: 0.8.10(solid-js@1.9.14)
'@testing-library/dom':
specifier: ^10.4.1
version: 10.4.1
@@ -65,6 +68,9 @@ importers:
effector-react:
specifier: ^23.3.0
version: 23.3.0(effector@23.4.4)(react@19.2.4)
+ effector-solid:
+ specifier: ^0.23.0
+ version: 0.23.0(effector@23.4.4)(solid-js@1.9.14)
eslint:
specifier: 10.0.2
version: 10.0.2(jiti@2.6.1)
@@ -104,6 +110,12 @@ importers:
react-native:
specifier: 0.84.1
version: 0.84.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4)
+ solid-js:
+ specifier: ^1.9.14
+ version: 1.9.14
+ typescript:
+ specifier: 5.8.2
+ version: 5.8.2
typescript-eslint:
specifier: ^8.56.1
version: 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.8.2)
@@ -116,6 +128,9 @@ importers:
vite-plugin-dts:
specifier: 4.5.4
version: 4.5.4(@types/node@25.3.3)(rollup@4.60.2)(typescript@5.8.2)(vite@7.3.2(@types/node@25.3.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))
+ vite-plugin-solid:
+ specifier: ^2.11.12
+ version: 2.11.12(solid-js@1.9.14)(vite@7.3.2(@types/node@25.3.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))
vitepress:
specifier: 1.6.4
version: 1.6.4(@algolia/client-search@5.49.1)(@types/node@25.3.3)(@types/react@19.2.14)(postcss@8.5.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3)(terser@5.46.0)(typescript@5.8.2)
@@ -148,6 +163,43 @@ importers:
specifier: 1.6.4
version: 1.6.4(@algolia/client-search@5.49.1)(@types/node@25.3.3)(@types/react@19.2.14)(postcss@8.5.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3)(terser@5.46.0)(typescript@5.8.2)
+ examples/solid-router:
+ dependencies:
+ '@effector/router':
+ specifier: workspace:*
+ version: link:../../packages/core
+ '@effector/router-solid':
+ specifier: workspace:*
+ version: link:../../packages/solid
+ effector:
+ specifier: ^23.4.4
+ version: 23.4.4
+ effector-solid:
+ specifier: ^0.23.0
+ version: 0.23.0(effector@23.4.4)(solid-js@1.9.14)
+ history:
+ specifier: 5.3.0
+ version: 5.3.0
+ solid-js:
+ specifier: ^1.9.14
+ version: 1.9.14
+ zod:
+ specifier: ^4.3.6
+ version: 4.3.6
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.52.0
+ version: 1.61.1
+ typescript:
+ specifier: 5.8.2
+ version: 5.8.2
+ vite:
+ specifier: 7.3.2
+ version: 7.3.2(@types/node@25.3.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)
+ vite-plugin-solid:
+ specifier: ^2.11.12
+ version: 2.11.12(solid-js@1.9.14)(vite@7.3.2(@types/node@25.3.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))
+
packages/core:
dependencies:
'@effector/router-paths':
@@ -216,6 +268,21 @@ importers:
specifier: '>=0.70'
version: 0.84.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4)
+ packages/solid:
+ dependencies:
+ '@effector/router':
+ specifier: workspace:^
+ version: link:../core
+ effector:
+ specifier: '>=23'
+ version: 23.4.4
+ effector-solid:
+ specifier: '>=0.23'
+ version: 0.23.0(effector@23.4.4)(solid-js@1.9.14)
+ solid-js:
+ specifier: ^1.9.0
+ version: 1.9.14
+
packages:
'@algolia/abtesting@1.15.1':
@@ -372,6 +439,10 @@ packages:
resolution: {integrity: sha512-xkXrMbtk87Gk7+oKBVmBc6EORg/Qwx++AHESldmHkpvG8wgccdhJJFwrzqlF382Fk8wfXhJHWE/g/43QvEGNPQ==}
engines: {node: ^22.18.0 || >=24.11.0}
+ '@babel/helper-module-imports@7.18.6':
+ resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-module-imports@7.28.6':
resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
engines: {node: '>=6.9.0'}
@@ -1736,6 +1807,11 @@ packages:
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ '@playwright/test@1.61.1':
+ resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
@@ -2056,6 +2132,16 @@ packages:
'@sinonjs/fake-timers@10.3.0':
resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==}
+ '@solidjs/testing-library@0.8.10':
+ resolution: {integrity: sha512-qdeuIerwyq7oQTIrrKvV0aL9aFeuwTd86VYD3afdq5HYEwoox1OBTJy4y8A3TFZr8oAR0nujYgCzY/8wgHGfeQ==}
+ engines: {node: '>= 14'}
+ peerDependencies:
+ '@solidjs/router': '>=0.9.0'
+ solid-js: '>=1.0.0'
+ peerDependenciesMeta:
+ '@solidjs/router':
+ optional: true
+
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -2581,6 +2667,11 @@ packages:
resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ babel-plugin-jsx-dom-expressions@0.40.7:
+ resolution: {integrity: sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==}
+ peerDependencies:
+ '@babel/core': ^7.20.12
+
babel-plugin-polyfill-corejs2@0.4.15:
resolution: {integrity: sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==}
peerDependencies:
@@ -2610,6 +2701,15 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
+ babel-preset-solid@1.9.12:
+ resolution: {integrity: sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ solid-js: ^1.9.12
+ peerDependenciesMeta:
+ solid-js:
+ optional: true
+
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -2880,6 +2980,13 @@ packages:
effector: ^23.0.0
react: '>=16.8.0 <20.0.0'
+ effector-solid@0.23.0:
+ resolution: {integrity: sha512-2y+tdrkAe4Av21bEtXjEZ+rSSKOGxDDJNbYIFW/y3Q9io5zsJSVtaF19/Smr7qYLD6SzhhKZEUUdWmtpfD9R9Q==}
+ engines: {node: '>=11.0.0'}
+ peerDependencies:
+ effector: ^23.0.0
+ solid-js: '>= 1.3.0'
+
effector@23.4.4:
resolution: {integrity: sha512-QkZboRN28K/iwxigDhlJcI3ux3aNbt8kYGGH/GkqWG0OlGeyuBhb7PdM89Iu+ogV8Lmz16xIlwnXR2UNWI6psg==}
engines: {node: '>=11.0.0'}
@@ -2905,6 +3012,10 @@ packages:
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
engines: {node: '>=8.6'}
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
@@ -3141,6 +3252,11 @@ packages:
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -3238,6 +3354,9 @@ packages:
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
+ html-entities@2.3.3:
+ resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==}
+
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
@@ -3324,6 +3443,10 @@ packages:
resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==}
engines: {node: '>=4'}
+ is-what@4.1.16:
+ resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
+ engines: {node: '>=12.13'}
+
is-what@5.5.0:
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
engines: {node: '>=18'}
@@ -3523,6 +3646,10 @@ packages:
memoize-one@5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
+ merge-anything@5.1.7:
+ resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==}
+ engines: {node: '>=12.13'}
+
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -3796,6 +3923,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ parse5@7.3.0:
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -3865,6 +3995,16 @@ packages:
pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
+ playwright-core@1.61.1:
+ resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.61.1:
+ resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
postcss@8.5.13:
resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==}
engines: {node: ^10 || ^12 || >=14}
@@ -4118,6 +4258,16 @@ packages:
resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==}
engines: {node: '>=0.10.0'}
+ seroval-plugins@1.5.4:
+ resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ seroval: ^1.0
+
+ seroval@1.5.4:
+ resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==}
+ engines: {node: '>=10'}
+
serve-static@1.16.3:
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
engines: {node: '>= 0.8.0'}
@@ -4165,6 +4315,14 @@ packages:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
+ solid-js@1.9.14:
+ resolution: {integrity: sha512-sAEXC0Kk0S1EDg+8ysEWJDbYhA3RRoEjwuySUGlKIemeo0I5YZfOyumNjNs9Sv3y2nmhD+0rW66ag2HsMuQiGQ==}
+
+ solid-refresh@0.6.3:
+ resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==}
+ peerDependencies:
+ solid-js: ^1.3
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -4494,6 +4652,16 @@ packages:
vite:
optional: true
+ vite-plugin-solid@2.11.12:
+ resolution: {integrity: sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA==}
+ peerDependencies:
+ '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.*
+ solid-js: ^1.7.2
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ '@testing-library/jest-dom':
+ optional: true
+
vite@5.4.21:
resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -4565,6 +4733,14 @@ packages:
yaml:
optional: true
+ vitefu@1.1.3:
+ resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==}
+ peerDependencies:
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
vitepress@1.6.4:
resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==}
hasBin: true
@@ -4979,6 +5155,10 @@ snapshots:
'@babel/traverse': 8.0.0
'@babel/types': 8.0.0
+ '@babel/helper-module-imports@7.18.6':
+ dependencies:
+ '@babel/types': 7.29.0
+
'@babel/helper-module-imports@7.28.6':
dependencies:
'@babel/traverse': 7.29.0
@@ -6435,6 +6615,10 @@ snapshots:
'@pkgr/core@0.2.9': {}
+ '@playwright/test@1.61.1':
+ dependencies:
+ playwright: 1.61.1
+
'@polka/url@1.0.0-next.29': {}
'@quansync/fs@1.0.0':
@@ -6748,6 +6932,11 @@ snapshots:
dependencies:
'@sinonjs/commons': 3.0.1
+ '@solidjs/testing-library@0.8.10(solid-js@1.9.14)':
+ dependencies:
+ '@testing-library/dom': 10.4.1
+ solid-js: 1.9.14
+
'@standard-schema/spec@1.1.0': {}
'@testing-library/dom@10.4.1':
@@ -7414,6 +7603,15 @@ snapshots:
'@types/babel__core': 7.20.5
'@types/babel__traverse': 7.28.0
+ babel-plugin-jsx-dom-expressions@0.40.7(@babel/core@7.29.0):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.18.6
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
+ '@babel/types': 7.29.0
+ html-entities: 2.3.3
+ parse5: 7.3.0
+
babel-plugin-polyfill-corejs2@0.4.15(@babel/core@7.29.0):
dependencies:
'@babel/compat-data': 7.29.0
@@ -7467,6 +7665,13 @@ snapshots:
babel-plugin-jest-hoist: 29.6.3
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0)
+ babel-preset-solid@1.9.12(@babel/core@7.29.0)(solid-js@1.9.14):
+ dependencies:
+ '@babel/core': 7.29.0
+ babel-plugin-jsx-dom-expressions: 0.40.7(@babel/core@7.29.0)
+ optionalDependencies:
+ solid-js: 1.9.14
+
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
@@ -7696,6 +7901,11 @@ snapshots:
react: 19.2.4
use-sync-external-store: 1.6.0(react@19.2.4)
+ effector-solid@0.23.0(effector@23.4.4)(solid-js@1.9.14):
+ dependencies:
+ effector: 23.4.4
+ solid-js: 1.9.14
+
effector@23.4.4: {}
electron-to-chromium@1.5.307: {}
@@ -7713,6 +7923,8 @@ snapshots:
ansi-colors: 4.1.3
strip-ansi: 6.0.1
+ entities@6.0.1: {}
+
entities@7.0.1: {}
error-stack-parser@2.1.4:
@@ -7996,6 +8208,9 @@ snapshots:
fs.realpath@1.0.0: {}
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -8105,6 +8320,8 @@ snapshots:
hookable@5.5.3: {}
+ html-entities@2.3.3: {}
+
html-void-elements@3.0.0: {}
http-errors@2.0.1:
@@ -8178,6 +8395,8 @@ snapshots:
dependencies:
better-path-resolve: 1.0.0
+ is-what@4.1.16: {}
+
is-what@5.5.0: {}
is-windows@1.0.2: {}
@@ -8416,6 +8635,10 @@ snapshots:
memoize-one@5.2.1: {}
+ merge-anything@5.1.7:
+ dependencies:
+ is-what: 4.1.16
+
merge-stream@2.0.0: {}
merge2@1.4.1: {}
@@ -8797,6 +9020,10 @@ snapshots:
dependencies:
callsites: 3.1.0
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.1
+
parseurl@1.3.3: {}
path-browserify@1.0.1: {}
@@ -8845,6 +9072,14 @@ snapshots:
exsolve: 1.0.8
pathe: 2.0.3
+ playwright-core@1.61.1: {}
+
+ playwright@1.61.1:
+ dependencies:
+ playwright-core: 1.61.1
+ optionalDependencies:
+ fsevents: 2.3.2
+
postcss@8.5.13:
dependencies:
nanoid: 3.3.15
@@ -9144,6 +9379,12 @@ snapshots:
serialize-error@2.1.0: {}
+ seroval-plugins@1.5.4(seroval@1.5.4):
+ dependencies:
+ seroval: 1.5.4
+
+ seroval@1.5.4: {}
+
serve-static@1.16.3:
dependencies:
encodeurl: 2.0.0
@@ -9194,6 +9435,21 @@ snapshots:
slash@3.0.0: {}
+ solid-js@1.9.14:
+ dependencies:
+ csstype: 3.2.3
+ seroval: 1.5.4
+ seroval-plugins: 1.5.4(seroval@1.5.4)
+
+ solid-refresh@0.6.3(solid-js@1.9.14):
+ dependencies:
+ '@babel/generator': 7.29.1
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/types': 7.29.0
+ solid-js: 1.9.14
+ transitivePeerDependencies:
+ - supports-color
+
source-map-js@1.2.1: {}
source-map-support@0.5.21:
@@ -9508,6 +9764,19 @@ snapshots:
- rollup
- supports-color
+ vite-plugin-solid@2.11.12(solid-js@1.9.14)(vite@7.3.2(@types/node@25.3.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@types/babel__core': 7.20.5
+ babel-preset-solid: 1.9.12(@babel/core@7.29.0)(solid-js@1.9.14)
+ merge-anything: 5.1.7
+ solid-js: 1.9.14
+ solid-refresh: 0.6.3(solid-js@1.9.14)
+ vite: 7.3.2(@types/node@25.3.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)
+ vitefu: 1.1.3(vite@7.3.2(@types/node@25.3.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))
+ transitivePeerDependencies:
+ - supports-color
+
vite@5.4.21(@types/node@25.3.3)(terser@5.46.0):
dependencies:
esbuild: 0.21.5
@@ -9533,6 +9802,10 @@ snapshots:
terser: 5.46.0
yaml: 2.8.2
+ vitefu@1.1.3(vite@7.3.2(@types/node@25.3.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)):
+ optionalDependencies:
+ vite: 7.3.2(@types/node@25.3.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)
+
vitepress@1.6.4(@algolia/client-search@5.49.1)(@types/node@25.3.3)(@types/react@19.2.14)(postcss@8.5.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3)(terser@5.46.0)(typescript@5.8.2):
dependencies:
'@docsearch/css': 3.8.2
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 4879875..f5ae699 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,5 +1,6 @@
packages:
- 'packages/*'
+ - 'examples/*'
- 'docs'
allowBuilds:
esbuild: true
diff --git a/tsconfig.json b/tsconfig.json
index 1549df7..ccb2127 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -105,5 +105,8 @@
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
- }
+ },
+ // Solid uses a different JSX runtime (jsxImportSource: "solid-js"); it is
+ // typechecked via packages/solid/tsconfig.json instead of the root react-jsx.
+ "exclude": ["packages/solid", "examples/solid-router"]
}