├── .prettierrc.json ├── .prettierignore ├── packages ├── react-just │ ├── __tests__ │ │ ├── use-client │ │ │ ├── fixtures │ │ │ │ └── plugin │ │ │ │ │ ├── not-valid.css │ │ │ │ │ ├── not-client.js │ │ │ │ │ ├── client.js │ │ │ │ │ ├── client.mjs │ │ │ │ │ ├── not-valid.cjs │ │ │ │ │ ├── not-valid.cts │ │ │ │ │ ├── client.mts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── client.jsx │ │ │ │ │ └── client.tsx │ │ │ └── transform │ │ │ │ ├── fixtures │ │ │ │ ├── directive │ │ │ │ │ ├── no-directive.js │ │ │ │ │ ├── invalid.js │ │ │ │ │ └── valid.js │ │ │ │ ├── export-named-declaration │ │ │ │ │ ├── class.js │ │ │ │ │ ├── identifier.js │ │ │ │ │ ├── function.js │ │ │ │ │ ├── array-pattern.js │ │ │ │ │ ├── assignment-pattern.js │ │ │ │ │ ├── object-pattern.js │ │ │ │ │ └── rest-element.js │ │ │ │ ├── export-default-declaration │ │ │ │ │ ├── value-declaration.js │ │ │ │ │ ├── class-declaration.js │ │ │ │ │ ├── identifier.js │ │ │ │ │ └── function-declaration.js │ │ │ │ ├── export-named-specifiers.js │ │ │ │ └── export-named-from-source.js │ │ │ │ ├── __snapshots__ │ │ │ │ └── directive.test.ts.snap │ │ │ │ ├── directive.test.ts │ │ │ │ └── exports.test.ts │ │ └── use-server │ │ │ └── transform │ │ │ ├── fixtures │ │ │ ├── export-named-declaration │ │ │ │ ├── identifier.js │ │ │ │ ├── function.js │ │ │ │ ├── class.js │ │ │ │ ├── array-pattern.js │ │ │ │ ├── assignment-pattern.js │ │ │ │ ├── object-pattern.js │ │ │ │ └── rest-element.js │ │ │ ├── directive │ │ │ │ ├── no-directive.js │ │ │ │ ├── valid-module-level.js │ │ │ │ └── invalid-module-level.js │ │ │ ├── export-default-declaration │ │ │ │ ├── identifier.js │ │ │ │ ├── value-declaration.js │ │ │ │ ├── function-declaration.js │ │ │ │ └── class-declaration.js │ │ │ ├── export-named-specifiers.js │ │ │ └── export-named-from-source.js │ │ │ ├── __snapshots__ │ │ │ └── directive.test.ts.snap │ │ │ ├── directive.test.ts │ │ │ └── exports.test.ts │ ├── src │ │ ├── server │ │ │ ├── node.ts │ │ │ └── error.ts │ │ ├── constants.ts │ │ ├── react-dom.d.ts │ │ ├── vite │ │ │ ├── use-client │ │ │ │ ├── transform │ │ │ │ │ ├── module.ts │ │ │ │ │ ├── export-named-specifiers.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── export-named-from-source.ts │ │ │ │ │ ├── export-default-declaration.ts │ │ │ │ │ └── generator.ts │ │ │ │ └── directive.ts │ │ │ ├── use-server │ │ │ │ ├── transform │ │ │ │ │ ├── module.ts │ │ │ │ │ ├── export-named-specifiers.ts │ │ │ │ │ ├── export-named-from-source.ts │ │ │ │ │ ├── export-default-declaration.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── generator.ts │ │ │ │ ├── directive.ts │ │ │ │ └── environments.ts │ │ │ ├── index.ts │ │ │ ├── utils.ts │ │ │ ├── build.ts │ │ │ └── css.ts │ │ ├── async-store │ │ │ └── node.ts │ │ ├── rsc-stream.ts │ │ ├── implementations.ts │ │ └── flight │ │ │ └── node.ts │ ├── tsconfig.json │ ├── types │ │ ├── server.d.ts │ │ ├── vite.d.ts │ │ ├── client.d.ts │ │ ├── fizz.node.d.ts │ │ ├── handle.node.d.ts │ │ ├── shared.d.ts │ │ └── flight.node.d.ts │ ├── vitest.config.ts │ ├── tsconfig.tests.json │ ├── tsconfig.src.json │ ├── tsconfig.build.json │ ├── README.md │ ├── rollup.config.js │ └── package.json ├── node │ ├── tsconfig.json │ ├── types │ │ ├── vite.d.ts │ │ └── handle.d.ts │ ├── README.md │ ├── src │ │ ├── constants.ts │ │ ├── handle.ts │ │ └── bin.ts │ ├── tsconfig.src.json │ ├── tsconfig.build.json │ ├── rollup.config.js │ └── package.json ├── router │ ├── tsconfig.json │ ├── src │ │ ├── cjs.ts │ │ ├── utils.ts │ │ ├── components │ │ │ ├── Route.tsx │ │ │ ├── LocationProvider.tsx │ │ │ ├── Link.tsx │ │ │ └── NavigateProvider.tsx │ │ ├── context │ │ │ ├── navigate.ts │ │ │ └── location.ts │ │ ├── hooks │ │ │ ├── use-params.ts │ │ │ ├── use-pathname.ts │ │ │ ├── use-search-params.ts │ │ │ ├── use-location.ts │ │ │ └── use-navigate.ts │ │ └── index.ts │ ├── tsconfig.src.json │ ├── tsconfig.build.json │ ├── package.json │ ├── rollup.config.js │ └── types │ │ └── index.d.ts └── vercel │ ├── tsconfig.json │ ├── types │ ├── vite.d.ts │ └── handle.d.ts │ ├── README.md │ ├── tsconfig.src.json │ ├── tsconfig.build.json │ ├── rollup.config.js │ ├── src │ └── handle.ts │ └── package.json ├── templates ├── node-ts │ ├── src │ │ ├── vite.d.ts │ │ ├── db.ts │ │ ├── ServerCounter.css │ │ ├── ClientCounter.css │ │ ├── ClientCounter.tsx │ │ ├── ServerCounter.tsx │ │ ├── index.css │ │ ├── assets │ │ │ └── vite.svg │ │ └── index.tsx │ ├── tsconfig.json │ ├── vite.config.ts │ └── package.json ├── vercel-ts │ ├── src │ │ ├── vite.d.ts │ │ ├── db.ts │ │ ├── ClientCounter.css │ │ ├── ServerCounter.css │ │ ├── ClientCounter.tsx │ │ ├── ServerCounter.tsx │ │ ├── index.css │ │ ├── assets │ │ │ └── vite.svg │ │ └── index.tsx │ ├── tsconfig.json │ ├── vite.config.ts │ └── package.json └── node-js │ ├── src │ ├── db.ts │ ├── ServerCounter.css │ ├── ClientCounter.css │ ├── ClientCounter.jsx │ ├── ServerCounter.jsx │ ├── index.css │ ├── assets │ │ └── vite.svg │ └── index.jsx │ ├── vite.config.js │ └── package.json ├── .gitignore ├── docs ├── public │ ├── favicon.ico │ ├── og-image.png │ ├── favicon-96x96.png │ └── images │ │ └── hero-snippet.png ├── vue.d.ts ├── reference │ ├── core │ │ ├── client.md │ │ ├── plugin.md │ │ └── server.md │ ├── platforms │ │ ├── vercel.md │ │ └── node.md │ ├── core.md │ ├── router │ │ ├── use-pathname.md │ │ ├── use-search-params.md │ │ ├── route-component-props.md │ │ ├── use-params.md │ │ ├── link.md │ │ ├── use-navigate.md │ │ ├── router.md │ │ └── route.md │ └── router.md ├── package.json ├── .vitepress │ └── theme │ │ ├── index.ts │ │ ├── components │ │ ├── HomeHero.vue │ │ ├── Card.vue │ │ ├── LevelIndicator.vue │ │ └── Home.vue │ │ ├── custom.css │ │ └── icons │ │ ├── next.svg │ │ ├── react-router-dark.svg │ │ └── react-router-light.svg ├── guide │ ├── app-component.md │ ├── server-functions.md │ ├── deploy.md │ ├── request-and-response.md │ ├── client-and-server-components.md │ ├── deploy │ │ ├── vercel.md │ │ └── node.md │ ├── tailwindcss.md │ ├── getting-started.md │ ├── routing.md │ └── installation.md └── reference.md ├── pnpm-workspace.yaml ├── package.json ├── README.md ├── ROADMAP.md └── LICENSE /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/fixtures/plugin/not-valid.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/node-ts/src/vite.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | docs/.vitepress/cache/ 5 | -------------------------------------------------------------------------------- /templates/vercel-ts/src/vite.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/fixtures/plugin/not-client.js: -------------------------------------------------------------------------------- 1 | export default 1; 2 | -------------------------------------------------------------------------------- /packages/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [{ "path": "./tsconfig.src.json" }] 3 | } 4 | -------------------------------------------------------------------------------- /packages/router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [{ "path": "./tsconfig.src.json" }] 3 | } 4 | -------------------------------------------------------------------------------- /packages/vercel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [{ "path": "./tsconfig.src.json" }] 3 | } 4 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almadoro/react-just/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almadoro/react-just/HEAD/docs/public/og-image.png -------------------------------------------------------------------------------- /packages/react-just/src/server/node.ts: -------------------------------------------------------------------------------- 1 | export { request, response } from "../async-store/node"; 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - docs 4 | 5 | linkWorkspacePackages: true 6 | -------------------------------------------------------------------------------- /docs/vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/directive/no-directive.js: -------------------------------------------------------------------------------- 1 | export default a; 2 | -------------------------------------------------------------------------------- /packages/router/src/cjs.ts: -------------------------------------------------------------------------------- 1 | throw new Error("This package should no be imported as a CommonJS module"); 2 | -------------------------------------------------------------------------------- /docs/public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almadoro/react-just/HEAD/docs/public/favicon-96x96.png -------------------------------------------------------------------------------- /packages/node/types/vite.d.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "vite"; 2 | 3 | export default function node(): Plugin; 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/fixtures/plugin/client.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export default 1; 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/fixtures/plugin/client.mjs: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export default 1; 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/fixtures/plugin/not-valid.cjs: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | module.exports = 1; 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/fixtures/plugin/not-valid.cts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | module.exports = 1; 4 | -------------------------------------------------------------------------------- /packages/vercel/types/vite.d.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "vite"; 2 | 3 | export default function vercel(): Plugin; 4 | -------------------------------------------------------------------------------- /docs/public/images/hero-snippet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almadoro/react-just/HEAD/docs/public/images/hero-snippet.png -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-named-declaration/class.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export class A {} 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-named-declaration/identifier.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export let a; 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-named-declaration/identifier.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export let a; 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/fixtures/plugin/client.mts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | let a: number | undefined; 4 | 5 | export default a; 6 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/fixtures/plugin/client.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | let a: number | undefined; 4 | 5 | export default a; 6 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/directive/invalid.js: -------------------------------------------------------------------------------- 1 | export default a; 2 | 3 | // prettier-ignore 4 | "use client"; 5 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/directive/valid.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | "use client"; 4 | 5 | export default a; 6 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-named-declaration/function.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export function a() {} 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-named-declaration/function.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export function a() {} 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-default-declaration/value-declaration.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export default 1; 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-named-declaration/class.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export class A extends Function {} 4 | -------------------------------------------------------------------------------- /packages/react-just/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const RSC_MIME_TYPE = "text/x-component"; 2 | 3 | export const RSC_FUNCTION_ID_HEADER = "x-rsc-function-id"; 4 | -------------------------------------------------------------------------------- /packages/react-just/src/server/error.ts: -------------------------------------------------------------------------------- 1 | throw new Error( 2 | '"react-just/server" can only be used within server components or server functions', 3 | ); 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/fixtures/plugin/client.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export default () => { 4 | return

Hello world

; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-default-declaration/class-declaration.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export default class A {} 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-default-declaration/identifier.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | let a; 4 | 5 | export default a; 6 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/directive/no-directive.js: -------------------------------------------------------------------------------- 1 | export default a; 2 | 3 | export { b }; 4 | 5 | export * from "pkg"; 6 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-default-declaration/identifier.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | let a; 4 | 5 | export default a; 6 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-default-declaration/value-declaration.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export default () => {}; 4 | -------------------------------------------------------------------------------- /packages/react-just/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.src.json" }, 4 | { "path": "./tsconfig.tests.json" } 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-default-declaration/function-declaration.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export default function () {} 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-default-declaration/function-declaration.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export default function () {} 4 | -------------------------------------------------------------------------------- /templates/node-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "bundler", 4 | "module": "esnext", 5 | "jsx": "preserve" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /templates/vercel-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "bundler", 4 | "module": "esnext", 5 | "jsx": "preserve" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/node/README.md: -------------------------------------------------------------------------------- 1 | # `@react-just/node` 2 | 3 | Official Node.js adapter for React Just. 4 | 5 | ## Documentation 6 | 7 | See [reactjust.dev](https://reactjust.dev) 8 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-named-declaration/array-pattern.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export let [a, { b }, [c], d = 1, , ...e] = []; 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-named-declaration/assignment-pattern.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export let [a = 1, { b } = c, [d] = e] = []; 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-default-declaration/class-declaration.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export default class A extends Function {} 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-named-declaration/array-pattern.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export let [a, { b }, [c], d = 1, , ...e] = []; 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-named-declaration/assignment-pattern.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export let [a = 1, { b } = c, [d] = e] = []; 4 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/directive/valid-module-level.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | "use server"; 4 | 5 | export default a; 6 | 7 | export { b }; 8 | -------------------------------------------------------------------------------- /packages/vercel/README.md: -------------------------------------------------------------------------------- 1 | # `@react-just/vercel` 2 | 3 | Official Vercel adapter for React Just. 4 | 5 | ## Documentation 6 | 7 | See [reactjust.dev](https://reactjust.dev) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "format": "prettier --write ." 5 | }, 6 | "devDependencies": { 7 | "prettier": "^3.5.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-named-specifiers.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | let a; 4 | let b; 5 | let d; 6 | 7 | export { a, b as c, d as default }; 8 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-named-specifiers.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | let a; 4 | let b; 5 | let d; 6 | 7 | export { a, b as c, d as default }; 8 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-named-from-source.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | let c = 1; 4 | 5 | export { a, b as c, b as d, default, default as e } from "pkg"; 6 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-named-from-source.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | let c = 1; 4 | 5 | export { a, b as c, b as d, default, default as e } from "pkg"; 6 | -------------------------------------------------------------------------------- /packages/react-just/src/react-dom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-dom/server.node" { 2 | import { renderToPipeableStream } from "react-dom/server"; 3 | 4 | export { renderToPipeableStream }; 5 | } 6 | -------------------------------------------------------------------------------- /packages/router/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function removeTrailingSlashes(inputUrl: URL) { 2 | const url = new URL(inputUrl); 3 | url.pathname = url.pathname.replace(/\/+$/g, ""); 4 | return url; 5 | } 6 | -------------------------------------------------------------------------------- /packages/router/src/components/Route.tsx: -------------------------------------------------------------------------------- 1 | import { RouteProps } from "@/types"; 2 | 3 | export default function Route(_: RouteProps): never { 4 | throw new Error("Route is not meant to be rendered"); 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/directive/invalid-module-level.js: -------------------------------------------------------------------------------- 1 | export default a; 2 | 3 | export { b }; 4 | 5 | export * from "pkg"; 6 | 7 | // prettier-ignore 8 | "use server"; 9 | -------------------------------------------------------------------------------- /packages/node/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_BUILD_PATH = "dist"; 2 | 3 | export const SERVER_ENTRY_FILENAME = "index.mjs"; 4 | 5 | export const SERVER_DIR = "server"; 6 | 7 | export const STATIC_DIR = "static"; 8 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-named-declaration/object-pattern.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export let { 4 | a, 5 | b: { c }, 6 | d: [e], 7 | f = 1, 8 | ...g 9 | } = {}; 10 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-named-declaration/object-pattern.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export let { 4 | a, 5 | b: { c }, 6 | d: [e], 7 | f = 1, 8 | ...g 9 | } = {}; 10 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/fixtures/export-named-declaration/rest-element.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export let [...a] = []; 4 | 5 | export let [...[b, c]] = []; 6 | 7 | export let [...{ d: e }] = []; 8 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/fixtures/export-named-declaration/rest-element.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export let [...a] = []; 4 | 5 | export let [...[b, c]] = []; 6 | 7 | export let [...{ d: e }] = []; 8 | -------------------------------------------------------------------------------- /templates/node-js/src/db.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | let count = 0; 4 | 5 | export async function getCount() { 6 | return count; 7 | } 8 | 9 | export async function incrementCount() { 10 | return ++count; 11 | } 12 | -------------------------------------------------------------------------------- /templates/node-ts/src/db.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | let count = 0; 4 | 5 | export async function getCount() { 6 | return count; 7 | } 8 | 9 | export async function incrementCount() { 10 | return ++count; 11 | } 12 | -------------------------------------------------------------------------------- /templates/vercel-ts/src/db.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | let count = 0; 4 | 5 | export async function getCount() { 6 | return count; 7 | } 8 | 9 | export async function incrementCount() { 10 | return ++count; 11 | } 12 | -------------------------------------------------------------------------------- /packages/router/src/context/navigate.ts: -------------------------------------------------------------------------------- 1 | import { Navigate } from "@/types"; 2 | import { createContext } from "react"; 3 | 4 | const NavigateContext = createContext(null); 5 | 6 | export default NavigateContext; 7 | -------------------------------------------------------------------------------- /templates/node-js/vite.config.js: -------------------------------------------------------------------------------- 1 | import react from "react-just/vite"; 2 | import node from "@react-just/node"; 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ 6 | plugins: [react(), node()], 7 | }); 8 | -------------------------------------------------------------------------------- /templates/node-ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "react-just/vite"; 2 | import node from "@react-just/node"; 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ 6 | plugins: [react(), node()], 7 | }); 8 | -------------------------------------------------------------------------------- /packages/react-just/types/server.d.ts: -------------------------------------------------------------------------------- 1 | import { JustRequest, JustResponse } from "./shared"; 2 | 3 | export function request(): JustRequest; 4 | 5 | export function response(): JustResponse; 6 | 7 | export { JustRequest, JustResponse }; 8 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/fixtures/plugin/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | export default () => { 6 | const str: string = "world"; 7 | 8 | return

Hello {str}

; 9 | }; 10 | -------------------------------------------------------------------------------- /templates/vercel-ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import vercel from "@react-just/vercel"; 2 | import react from "react-just/vite"; 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ 6 | plugins: [react(), vercel()], 7 | }); 8 | -------------------------------------------------------------------------------- /docs/reference/core/client.md: -------------------------------------------------------------------------------- 1 | # Client API (`react-just/client`) 2 | 3 | :::warning Low-level API 4 | This is considered a low-level API. It's **not intended for direct use** in applications. Use it only when building custom routing. 5 | ::: 6 | 7 | TODO 8 | -------------------------------------------------------------------------------- /packages/router/src/context/location.ts: -------------------------------------------------------------------------------- 1 | import { Params } from "@/types"; 2 | import { createContext } from "react"; 3 | 4 | const LocationContext = createContext<{ params: Params; url: string } | null>( 5 | null, 6 | ); 7 | 8 | export default LocationContext; 9 | -------------------------------------------------------------------------------- /packages/react-just/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | resolve: { 6 | alias: { 7 | "@": path.resolve(import.meta.dirname, "./src"), 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/router/src/hooks/use-params.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Params } from "@/types"; 4 | import useLocation from "./use-location"; 5 | 6 | export default function useParams(): Params { 7 | const location = useLocation(); 8 | 9 | return location.params; 10 | } 11 | -------------------------------------------------------------------------------- /packages/node/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "strict": true, 5 | "target": "esnext", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "paths": { 9 | "@/types/*": ["./types/*"] 10 | } 11 | }, 12 | "include": ["src", "types"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/router/src/hooks/use-pathname.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import useLocation from "./use-location"; 5 | 6 | export default function usePathname(): string { 7 | const location = useLocation(); 8 | 9 | return useMemo(() => new URL(location.url).pathname, [location.url]); 10 | } 11 | -------------------------------------------------------------------------------- /packages/vercel/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "strict": true, 5 | "target": "esnext", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "paths": { 9 | "@/types/*": ["./types/*"] 10 | } 11 | }, 12 | "include": ["src", "types"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/node/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "bundler", 7 | "skipLibCheck": true, 8 | "paths": { 9 | "@/types/*": ["./types/*"] 10 | } 11 | }, 12 | "include": ["src", "types"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/vercel/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "bundler", 7 | "skipLibCheck": true, 8 | "paths": { 9 | "@/types/*": ["./types/*"] 10 | } 11 | }, 12 | "include": ["src", "types"] 13 | } 14 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vitepress dev", 7 | "build": "vitepress build", 8 | "preview": "vitepress preview" 9 | }, 10 | "devDependencies": { 11 | "vitepress": "^1.6.3", 12 | "vitepress-plugin-group-icons": "^1.5.5" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/react-just/tsconfig.tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "strict": true, 5 | "target": "esnext", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | } 11 | }, 12 | "include": ["src", "__tests__", "vitest.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/router/src/hooks/use-search-params.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import useLocation from "./use-location"; 5 | 6 | export default function useSearchParams(): URLSearchParams { 7 | const location = useLocation(); 8 | 9 | return useMemo(() => new URL(location.url).searchParams, [location.url]); 10 | } 11 | -------------------------------------------------------------------------------- /packages/router/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "strict": true, 5 | "target": "esnext", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "jsx": "react-jsx", 9 | "paths": { 10 | "@/types": ["./types/index.d.ts"] 11 | } 12 | }, 13 | "include": ["src", "types"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/router/src/hooks/use-location.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { use } from "react"; 4 | import LocationContext from "../context/location"; 5 | 6 | export default function useLocation() { 7 | const location = use(LocationContext); 8 | 9 | if (!location) throw new Error("useLocation must be used within a Router"); 10 | 11 | return location; 12 | } 13 | -------------------------------------------------------------------------------- /packages/router/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "bundler", 7 | "skipLibCheck": true, 8 | "jsx": "react-jsx", 9 | "paths": { 10 | "@/types": ["./types/index.d.ts"] 11 | } 12 | }, 13 | "include": ["src", "types"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/react-just/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "strict": true, 5 | "target": "esnext", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "paths": { 9 | "@/types": ["./types/index.d.ts"], 10 | "@/types/*": ["./types/*"] 11 | } 12 | }, 13 | "include": ["src", "types"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/react-just/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "bundler", 7 | "skipLibCheck": true, 8 | "paths": { 9 | "@/types": ["./types/index.d.ts"], 10 | "@/types/*": ["./types/*"] 11 | } 12 | }, 13 | "include": ["src", "types"] 14 | } 15 | -------------------------------------------------------------------------------- /templates/node-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vite", 4 | "build": "vite build", 5 | "start": "react-just-node" 6 | }, 7 | "dependencies": { 8 | "@react-just/node": "^0.4.0", 9 | "react": "~19.1", 10 | "react-dom": "~19.1", 11 | "react-just": "^0.4.0" 12 | }, 13 | "devDependencies": { 14 | "vite": "^7.1.7" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/vercel-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vite", 4 | "build": "vite build" 5 | }, 6 | "dependencies": { 7 | "react": "~19.1", 8 | "react-dom": "~19.1", 9 | "react-just": "^0.4.0" 10 | }, 11 | "devDependencies": { 12 | "@react-just/vercel": "^0.3.0", 13 | "@types/react": "~19.1", 14 | "vite": "^7.1.7" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/node-js/src/ServerCounter.css: -------------------------------------------------------------------------------- 1 | .server-counter { 2 | border: none; 3 | border-radius: 8px; 4 | padding: 12px 20px; 5 | font-size: 1em; 6 | font-weight: 700; 7 | cursor: pointer; 8 | color: #ffffff; 9 | background-color: oklch(50% 0.134 242.749); 10 | transition: background-color 0.25s; 11 | } 12 | 13 | .server-counter:hover { 14 | background-color: oklch(55% 0.169 243.123); 15 | } 16 | -------------------------------------------------------------------------------- /templates/node-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vite", 4 | "build": "vite build", 5 | "start": "react-just-node" 6 | }, 7 | "dependencies": { 8 | "@react-just/node": "^0.4.0", 9 | "react": "~19.1", 10 | "react-dom": "~19.1", 11 | "react-just": "^0.4.0" 12 | }, 13 | "devDependencies": { 14 | "@types/react": "~19.1", 15 | "vite": "^7.1.7" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /templates/node-ts/src/ServerCounter.css: -------------------------------------------------------------------------------- 1 | .server-counter { 2 | border: none; 3 | border-radius: 8px; 4 | padding: 12px 20px; 5 | font-size: 1em; 6 | font-weight: 700; 7 | cursor: pointer; 8 | color: #ffffff; 9 | background-color: oklch(50% 0.134 242.749); 10 | transition: background-color 0.25s; 11 | } 12 | 13 | .server-counter:hover { 14 | background-color: oklch(55% 0.169 243.123); 15 | } 16 | -------------------------------------------------------------------------------- /packages/router/src/hooks/use-navigate.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Navigate } from "@/types"; 4 | import { use } from "react"; 5 | import NavigateContext from "../context/navigate"; 6 | 7 | export default function useNavigate(): Navigate { 8 | const navigate = use(NavigateContext); 9 | 10 | if (!navigate) throw new Error("useNavigate must be used within a Router"); 11 | 12 | return navigate; 13 | } 14 | -------------------------------------------------------------------------------- /templates/node-js/src/ClientCounter.css: -------------------------------------------------------------------------------- 1 | .client-counter { 2 | border: none; 3 | border-radius: 8px; 4 | padding: 12px 20px; 5 | font-size: 1em; 6 | font-weight: 700; 7 | cursor: pointer; 8 | color: #ffffff; 9 | background-color: oklch(43.2% 0.095 166.913); 10 | transition: background-color 0.25s; 11 | } 12 | 13 | .client-counter:hover { 14 | background-color: oklch(48.2% 0.151 167.153); 15 | } 16 | -------------------------------------------------------------------------------- /templates/node-ts/src/ClientCounter.css: -------------------------------------------------------------------------------- 1 | .client-counter { 2 | border: none; 3 | border-radius: 8px; 4 | padding: 12px 20px; 5 | font-size: 1em; 6 | font-weight: 700; 7 | cursor: pointer; 8 | color: #ffffff; 9 | background-color: oklch(43.2% 0.095 166.913); 10 | transition: background-color 0.25s; 11 | } 12 | 13 | .client-counter:hover { 14 | background-color: oklch(48.2% 0.151 167.153); 15 | } 16 | -------------------------------------------------------------------------------- /templates/vercel-ts/src/ClientCounter.css: -------------------------------------------------------------------------------- 1 | .client-counter { 2 | border: none; 3 | border-radius: 8px; 4 | padding: 12px 20px; 5 | font-size: 1em; 6 | font-weight: 700; 7 | cursor: pointer; 8 | color: #ffffff; 9 | background-color: oklch(43.2% 0.095 166.913); 10 | transition: background-color 0.25s; 11 | } 12 | 13 | .client-counter:hover { 14 | background-color: oklch(48.2% 0.151 167.153); 15 | } 16 | -------------------------------------------------------------------------------- /templates/vercel-ts/src/ServerCounter.css: -------------------------------------------------------------------------------- 1 | .server-counter { 2 | border: none; 3 | border-radius: 8px; 4 | padding: 12px 20px; 5 | font-size: 1em; 6 | font-weight: 700; 7 | cursor: pointer; 8 | color: #ffffff; 9 | background-color: oklch(50% 0.134 242.749); 10 | transition: background-color 0.25s; 11 | } 12 | 13 | .server-counter:hover { 14 | background-color: oklch(55% 0.169 243.123); 15 | } 16 | -------------------------------------------------------------------------------- /templates/node-js/src/ClientCounter.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import "./ClientCounter.css"; 4 | 5 | import { useState } from "react"; 6 | 7 | export default function ClientCounter() { 8 | const [count, setCount] = useState(0); 9 | 10 | return ( 11 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /templates/node-ts/src/ClientCounter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import "./ClientCounter.css"; 4 | 5 | import { useState } from "react"; 6 | 7 | export default function ClientCounter() { 8 | const [count, setCount] = useState(0); 9 | 10 | return ( 11 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /templates/vercel-ts/src/ClientCounter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import "./ClientCounter.css"; 4 | 5 | import { useState } from "react"; 6 | 7 | export default function ClientCounter() { 8 | const [count, setCount] = useState(0); 9 | 10 | return ( 11 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import "virtual:group-icons.css"; 2 | import { Theme } from "vitepress"; 3 | import DefaultTheme from "vitepress/theme"; 4 | import Card from "./components/Card.vue"; 5 | import Home from "./components/Home.vue"; 6 | import "./custom.css"; 7 | 8 | export default { 9 | extends: DefaultTheme, 10 | enhanceApp({ app }) { 11 | app.component("Card", Card); 12 | app.component("Home", Home); 13 | }, 14 | } satisfies Theme; 15 | -------------------------------------------------------------------------------- /packages/router/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Link } from "./components/Link"; 2 | export { default as Route } from "./components/Route"; 3 | export { default as Router } from "./components/Router"; 4 | export { default as useNavigate } from "./hooks/use-navigate"; 5 | export { default as useParams } from "./hooks/use-params"; 6 | export { default as usePathname } from "./hooks/use-pathname"; 7 | export { default as useSearchParams } from "./hooks/use-search-params"; 8 | -------------------------------------------------------------------------------- /packages/react-just/types/vite.d.ts: -------------------------------------------------------------------------------- 1 | import { PluginOption } from "vite"; 2 | 3 | export interface ReactJustOptions { 4 | app?: string; 5 | } 6 | 7 | export default function react(options?: ReactJustOptions): PluginOption; 8 | 9 | export const ENVIRONMENTS: { 10 | CLIENT: string; 11 | FIZZ_NODE: string; 12 | FLIGHT_NODE: string; 13 | }; 14 | 15 | export const ENTRIES: { 16 | CLIENT: string; 17 | FIZZ_NODE: string; 18 | FLIGHT_NODE: string; 19 | }; 20 | -------------------------------------------------------------------------------- /templates/node-js/src/ServerCounter.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useActionState } from "react"; 4 | import { incrementCount } from "./db"; 5 | import "./ServerCounter.css"; 6 | 7 | export default function ServerCounter({ initialCount }) { 8 | const [count, formAction] = useActionState(incrementCount, initialCount); 9 | 10 | return ( 11 |
12 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /templates/node-ts/src/ServerCounter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useActionState } from "react"; 4 | import { incrementCount } from "./db"; 5 | import "./ServerCounter.css"; 6 | 7 | export default function ServerCounter({ 8 | initialCount, 9 | }: { 10 | initialCount: number; 11 | }) { 12 | const [count, formAction] = useActionState(incrementCount, initialCount); 13 | 14 | return ( 15 |
16 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /templates/vercel-ts/src/ServerCounter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useActionState } from "react"; 4 | import { incrementCount } from "./db"; 5 | import "./ServerCounter.css"; 6 | 7 | export default function ServerCounter({ 8 | initialCount, 9 | }: { 10 | initialCount: number; 11 | }) { 12 | const [count, formAction] = useActionState(incrementCount, initialCount); 13 | 14 | return ( 15 |
16 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/router/src/components/LocationProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Params } from "@/types"; 4 | import { useMemo } from "react"; 5 | import LocationContext from "../context/location"; 6 | 7 | interface LocationProviderProps { 8 | children: React.ReactNode; 9 | params: Params; 10 | url: string; 11 | } 12 | 13 | export default function LocationProvider({ 14 | children, 15 | params, 16 | url, 17 | }: LocationProviderProps) { 18 | const value = useMemo(() => ({ url, params }), [url, params]); 19 | 20 | return {children}; 21 | } 22 | -------------------------------------------------------------------------------- /docs/reference/platforms/vercel.md: -------------------------------------------------------------------------------- 1 | # Vercel Adapter (`@react-just/vercel`) 2 | 3 | ## Plugin Usage 4 | 5 | ```ts [vite.config.ts] {1,6} 6 | import vercel from "@react-just/vercel"; 7 | import react from "react-just/vite"; 8 | import { defineConfig } from "vite"; 9 | 10 | export default defineConfig({ 11 | plugins: [react(), vercel()], 12 | }); 13 | ``` 14 | 15 | ### Output 16 | 17 | When you run `vite build`, the adapter generates a `.vercel/output` directory in your project root. This folder conforms to Vercel’s Build Output API specification and can be deployed directly to Vercel without additional configuration. 18 | -------------------------------------------------------------------------------- /packages/react-just/types/client.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export function createFromRscFetch(res: Promise): PromiseLike; 4 | 5 | export function hydrateFromWindowStream(): Promise; 6 | 7 | export function registerClientReference( 8 | implementation: unknown, 9 | moduleId: string | number, 10 | exportName: string | number, 11 | ): unknown; 12 | 13 | export function registerServerReference( 14 | id: string, 15 | ): (...args: TArgs) => Promise; 16 | 17 | export function render(tree: ReactNode): void; 18 | 19 | export declare const RSC_MIME_TYPE: string; 20 | -------------------------------------------------------------------------------- /packages/react-just/README.md: -------------------------------------------------------------------------------- 1 | # `react-just` 2 | 3 | Vite plugin to enable React with React Server Components support. 4 | 5 | ## Documentation 6 | 7 | See [reactjust.dev](https://reactjust.dev) 8 | 9 | ## Usage 10 | 11 | ```ts 12 | // vite.config.ts 13 | import react from "react-just/vite"; 14 | import { defineConfig } from "vite"; 15 | 16 | export default defineConfig({ 17 | plugins: [react()], 18 | }); 19 | ``` 20 | 21 | ```tsx 22 | // src/index.tsx 23 | export default function App() { 24 | return ( 25 | 26 | 27 |

Hello from a server component

28 | 29 | 30 | ); 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /packages/react-just/types/fizz.node.d.ts: -------------------------------------------------------------------------------- 1 | import { PipeableStream, ReactFormState } from "./shared"; 2 | 3 | export function registerClientReference( 4 | implementation: unknown, 5 | moduleId: string | number, 6 | exportName: string | number, 7 | ): unknown; 8 | 9 | export function registerServerReference(id: string): unknown; 10 | 11 | export function renderToPipeableStream( 12 | rscStream: PipeableStream, 13 | options: RenderToPipeableStreamOptions, 14 | ): PipeableStream; 15 | 16 | export interface RenderToPipeableStreamOptions { 17 | formState: ReactFormState | null; 18 | onShellError: (error: unknown) => void; 19 | onShellReady: () => void; 20 | } 21 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-client/transform/module.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ModuleDeclaration, Program, Statement } from "estree"; 2 | 3 | export default class Module { 4 | constructor(private program: Program) {} 5 | 6 | public append(...nodes: BodyNode[]) { 7 | this.program.body.push(...nodes); 8 | } 9 | 10 | public remove(node: BodyNode) { 11 | this.program.body.splice(this.program.body.indexOf(node), 1); 12 | } 13 | 14 | public replace(node: BodyNode, newNode: BodyNode) { 15 | this.program.body.splice(this.program.body.indexOf(node), 1, newNode); 16 | } 17 | 18 | public unshift(...nodes: BodyNode[]) { 19 | this.program.body.unshift(...nodes); 20 | } 21 | } 22 | 23 | type BodyNode = Statement | Directive | ModuleDeclaration; 24 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-server/transform/module.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ModuleDeclaration, Program, Statement } from "estree"; 2 | 3 | export default class Module { 4 | constructor(private program: Program) {} 5 | 6 | public append(...nodes: BodyNode[]) { 7 | this.program.body.push(...nodes); 8 | } 9 | 10 | public remove(node: BodyNode) { 11 | this.program.body.splice(this.program.body.indexOf(node), 1); 12 | } 13 | 14 | public replace(node: BodyNode, newNode: BodyNode) { 15 | this.program.body.splice(this.program.body.indexOf(node), 1, newNode); 16 | } 17 | 18 | public unshift(...nodes: BodyNode[]) { 19 | this.program.body.unshift(...nodes); 20 | } 21 | } 22 | 23 | type BodyNode = Statement | Directive | ModuleDeclaration; 24 | -------------------------------------------------------------------------------- /packages/vercel/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from "@rollup/plugin-node-resolve"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | 5 | export default defineConfig({ 6 | input: { handle: "src/handle.ts", vite: "src/vite.ts" }, 7 | output: [ 8 | { 9 | dir: "dist", 10 | format: "esm", 11 | entryFileNames: "[name].mjs", 12 | chunkFileNames: "[name].mjs", 13 | }, 14 | { 15 | dir: "dist", 16 | format: "cjs", 17 | entryFileNames: "[name].cjs", 18 | chunkFileNames: "[name].cjs", 19 | }, 20 | ], 21 | external: (id) => /node_modules/.test(id) || id.startsWith("react-just"), 22 | plugins: [typescript({ tsconfig: "tsconfig.build.json" }), nodeResolve()], 23 | }); 24 | -------------------------------------------------------------------------------- /docs/guide/app-component.md: -------------------------------------------------------------------------------- 1 | # App Component 2 | 3 | The App Component serves as the main entry point of your application. By default, React Just looks for the following files in your project root to use as the module containing the App Component: 4 | 5 | - `src/index.tsx` 6 | - `src/index.jsx` 7 | - `src/index.ts` 8 | - `src/index.js` 9 | 10 | You can override this behavior with the `app` property in the [plugin options](/reference/core/plugin). 11 | 12 | It **must** be a Server Component, exported as `default` from its module, and always return at least the `html` and `body` tags. 13 | 14 | ```tsx [src/index.tsx] {1,3,4,6,7} 15 | export default function App() { 16 | return ( 17 | 18 | 19 |

Hello World

20 | 21 | 22 | ); 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/reference/core/plugin.md: -------------------------------------------------------------------------------- 1 | # Plugin (`react-just/vite`) 2 | 3 | This Vite plugin enables React with React Server Components (RSC) support and provides a development server. 4 | 5 | ```ts [vite.config.ts] {1,5} 6 | import react from "react-just/vite"; 7 | import { defineConfig } from "vite"; 8 | 9 | export default defineConfig({ 10 | plugins: [react()], 11 | }); 12 | ``` 13 | 14 | ## Options 15 | 16 | ```ts 17 | react(options?: ReactJustOptions); 18 | 19 | interface ReactJustOptions = { 20 | app?: string; 21 | }; 22 | ``` 23 | 24 | The plugin accepts an _optional_ `options` object with the following properties: 25 | 26 | - `app`: Path to the module that exports the App Component. By default, first matching file among: 27 | - `src/index.tsx` 28 | - `src/index.jsx` 29 | - `src/index.ts` 30 | - `src/index.js` 31 | -------------------------------------------------------------------------------- /packages/react-just/src/async-store/node.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "@/types/flight.node"; 2 | import { AsyncLocalStorage } from "node:async_hooks"; 3 | 4 | const asyncLocalStorage = new AsyncLocalStorage(); 5 | 6 | export function request() { 7 | const store = asyncLocalStorage.getStore(); 8 | 9 | if (!store) 10 | throw new Error("request must be called within a request context"); 11 | 12 | return store.req; 13 | } 14 | 15 | export function response() { 16 | const store = asyncLocalStorage.getStore(); 17 | 18 | if (!store) 19 | throw new Error("response must be called within a request context"); 20 | 21 | return store.res; 22 | } 23 | 24 | export async function runWithContext unknown>( 25 | context: Context, 26 | fn: T, 27 | ): Promise { 28 | await asyncLocalStorage.run(context, fn); 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Just 2 | 3 | **The simplest way to use React Server Components**. It's a Vite plugin to enable React with React Server Components support. 4 | 5 | ## Getting Started 6 | 7 | If you're interested in using React Just, follow the guide [here](https://reactjust.dev). If you're interested in the code, keep reading. 8 | 9 | ## Packages 10 | 11 | This monorepo contains the following packages: 12 | 13 | | Package | Description | 14 | | ----------------------------------------- | ---------------------------------------- | 15 | | [`react-just`](./packages/react-just) | Core plugin and low level utilities APIs | 16 | | [`@react-just/node`](./packages/node) | Node.js adapter | 17 | | [`@react-just/vercel`](./packages/vercel) | Vercel adapter | 18 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/__snapshots__/directive.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`'use client' directive > throws and error when the directive is not at the top of the file and doesn't modify the program 1`] = ` 4 | "export default a; 5 | "use client"; 6 | " 7 | `; 8 | 9 | exports[`'use client' directive > throws and error when there is no directive and doesn't modify the program 1`] = ` 10 | "export default a; 11 | " 12 | `; 13 | 14 | exports[`'use client' directive > transforms when the directive is found at the top of the file or after other directives 1`] = ` 15 | "import {registerClientReference as $$registerClientReference$$} from "react-just"; 16 | "use strict"; 17 | const $$Ref$$default = $$registerClientReference$$("default"); 18 | export {$$Ref$$default as default}; 19 | " 20 | `; 21 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/HomeHero.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | -------------------------------------------------------------------------------- /packages/react-just/src/rsc-stream.ts: -------------------------------------------------------------------------------- 1 | // Inspired by next.js implementation 2 | // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/use-flight-response.tsx 3 | // https://github.com/vercel/next.js/blob/canary/packages/next/src/client/app-index.tsx 4 | 5 | export const RSC_STREAM_WINDOW_IDENTIFIER = "__RJS__"; 6 | 7 | export const RSC_STREAM_STRING_DATA = 1; 8 | export const RSC_STREAM_BINARY_DATA = 2; 9 | 10 | export type RscStreamChunk = StringDataChunk | BinaryDataChunk; 11 | 12 | export type StringDataChunk = [ 13 | type: typeof RSC_STREAM_STRING_DATA, 14 | str: string, 15 | ]; 16 | 17 | export type BinaryDataChunk = [ 18 | type: typeof RSC_STREAM_BINARY_DATA, 19 | base64: string, 20 | ]; 21 | 22 | declare global { 23 | interface Window { 24 | [RSC_STREAM_WINDOW_IDENTIFIER]: Iterable & { 25 | push: (chunk: RscStreamChunk) => void; 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Component Libraries 2 | 3 | - Document Mantine. 4 | - MUI 5 | - Document. 6 | - Create complementary package. 7 | - Add feature to ignore \`export \*\` for node_modules packages. 8 | 9 | # Improve Router 10 | 11 | - Fragment support. 12 | - Layouts (no-path Route). 13 | - Check what happens when navigating and connection is lost. 14 | 15 | # Error handling 16 | 17 | - Add support for error component on fatal error. 18 | 19 | # Code Splitting 20 | 21 | - Support for React.lazy. 22 | - Support for CSS modules. 23 | 24 | # Assets support 25 | 26 | - Check server assets handling. 27 | 28 | # Use server 29 | 30 | - `use server` directive support. 31 | - External modules support. 32 | 33 | # Optimization 34 | 35 | - Optimize dev commnad by no forcing dependencies reoptimization on flight env. 36 | 37 | # Security 38 | 39 | - Add `nonce` to scripts. 40 | 41 | # Platforms 42 | 43 | - Netlify 44 | - Cloudflare 45 | - Bun 46 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-c-brand-1: #1ab680; 3 | --vp-c-brand-2: #1ab680; 4 | --vp-c-brand-3: #1ab680; 5 | --vp-c-brand-soft: #00bc8d; 6 | --vp-button-brand-bg: #00a67d; 7 | --vp-button-brand-hover-bg: #00bc8d; 8 | --vp-home-hero-name-color: transparent; 9 | --vp-home-hero-name-background: -webkit-linear-gradient( 10 | -120deg, 11 | #00a67d, 12 | #00ffbf 13 | ); 14 | } 15 | 16 | .dark { 17 | --vp-c-brand-1: #00ffbf; 18 | --vp-c-brand-2: #00ffbf; 19 | --vp-c-brand-3: #00ffbf; 20 | --vp-c-brand-soft: #00bc8d; 21 | --vp-button-brand-bg: #00a67d; 22 | --vp-button-brand-hover-bg: #00bc8d; 23 | --vp-c-bg: #16181d; 24 | --vp-c-bg-alt: #111316; 25 | --vp-c-divider: #343a46; 26 | --vp-c-gutter: #343a46; 27 | --vp-home-hero-name-background: -webkit-linear-gradient( 28 | -120deg, 29 | #00a67d, 30 | #1ab680 31 | ); 32 | } 33 | 34 | html { 35 | scroll-behavior: smooth; 36 | } 37 | -------------------------------------------------------------------------------- /docs/guide/server-functions.md: -------------------------------------------------------------------------------- 1 | # Server Functions 2 | 3 | You can use the `use server` directive at the top of a module to mark all exported functions as **Server Functions**. 4 | 5 | ```ts [src/api/posts.ts] 6 | "use server"; 7 | 8 | export function createComment(formData: FormData) { 9 | // ... 10 | } 11 | 12 | export function deleteComment(id: string) { 13 | // ... 14 | } 15 | ``` 16 | 17 | ```tsx [src/Post.tsx] 18 | import { createComment } from "./api/posts"; 19 | 20 | function Post() { 21 | return ( 22 | // ... 23 |
24 | 25 |
26 | ); 27 | } 28 | ``` 29 | 30 | ::::warning Function Level Directive 31 | Currently, the `'use server'` directive is only supported at the module level. Support for function-level directives is under development. 32 | :::: 33 | 34 | To learn more about Server Components, see the [React documentation](https://react.dev/reference/rsc/server-functions). 35 | -------------------------------------------------------------------------------- /packages/react-just/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from "@rollup/plugin-node-resolve"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | 5 | export default defineConfig({ 6 | input: { 7 | client: "src/client.ts", 8 | "fizz.node": "src/fizz/node.ts", 9 | "flight.node": "src/flight/node.ts", 10 | "handle.node": "src/handle/node.ts", 11 | "server.error": "src/server/error.ts", 12 | "server.node": "src/server/node.ts", 13 | vite: "src/vite/index.ts", 14 | }, 15 | output: [ 16 | { 17 | dir: "dist", 18 | format: "cjs", 19 | entryFileNames: "[name].cjs", 20 | chunkFileNames: "[name].cjs", 21 | }, 22 | { 23 | dir: "dist", 24 | format: "esm", 25 | entryFileNames: "[name].mjs", 26 | chunkFileNames: "[name].mjs", 27 | }, 28 | ], 29 | external: /node_modules/, 30 | plugins: [typescript({ tsconfig: "tsconfig.build.json" }), nodeResolve()], 31 | }); 32 | -------------------------------------------------------------------------------- /docs/reference/core.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: false 3 | --- 4 | 5 | # Core (`react-just`) 6 | 7 |
8 | 9 |
10 | 11 | ## Server Utilities (`react-just/server`) 12 | 13 |
14 | 15 | 16 |
17 | 18 | ## Low-Level APIs 19 | 20 |
21 | 22 |
23 | 24 | 38 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: false 3 | --- 4 | 5 | # Reference 6 | 7 |
8 | 9 | 10 |
11 | 12 | ## Platforms 13 | 14 |
15 | 16 | 17 |
18 | 19 | 33 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/__snapshots__/directive.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`'use server' module level directive > throws an error when the directive is not at the top of the file and doesn't modify the program 1`] = ` 4 | "export default a; 5 | export {b}; 6 | export * from "pkg"; 7 | "use server"; 8 | " 9 | `; 10 | 11 | exports[`'use server' module level directive > throws an error when there is no directive and doesn't modify the program 1`] = ` 12 | "export default a; 13 | export {b}; 14 | export * from "pkg"; 15 | " 16 | `; 17 | 18 | exports[`'use server' module level directive > transforms when the directive is found at the top of the file or after other directives 1`] = ` 19 | "import {registerServerReference as $$registerServerReference$$} from "react-just"; 20 | "use strict"; 21 | const $$Ref$$default = $$registerServerReference$$("default"); 22 | export {$$Ref$$default as default}; 23 | const $$Ref$$b = $$registerServerReference$$("b"); 24 | export {$$Ref$$b as b}; 25 | " 26 | `; 27 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/icons/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/react-just/types/handle.node.d.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from "node:http"; 2 | import React, { ComponentType } from "react"; 3 | import { renderToPipeableStream as renderToPipeableHtmlStream } from "./fizz.node"; 4 | import { 5 | createTemporaryReferenceSet, 6 | decodeAction, 7 | decodeFormState, 8 | decodeReply, 9 | renderToPipeableStream as renderToPipeableRscStream, 10 | runWithContext, 11 | } from "./flight.node"; 12 | 13 | export interface HandleOptions { 14 | App: ComponentType; 15 | createTemporaryReferenceSet: typeof createTemporaryReferenceSet; 16 | decodeAction: typeof decodeAction; 17 | decodeFormState: typeof decodeFormState; 18 | decodeReply: typeof decodeReply; 19 | React: typeof React; 20 | renderToPipeableHtmlStream: typeof renderToPipeableHtmlStream; 21 | renderToPipeableRscStream: typeof renderToPipeableRscStream; 22 | runWithContext: typeof runWithContext; 23 | onShellError?: (error: unknown) => void; 24 | } 25 | 26 | export function createHandle( 27 | options: HandleOptions, 28 | ): (req: IncomingMessage, res: ServerResponse) => void; 29 | -------------------------------------------------------------------------------- /packages/node/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from "@rollup/plugin-node-resolve"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | 5 | export default defineConfig([ 6 | { 7 | input: { bin: "src/bin.ts", handle: "src/handle.ts", vite: "src/vite.ts" }, 8 | output: [ 9 | { 10 | dir: "dist", 11 | format: "esm", 12 | entryFileNames: "[name].mjs", 13 | chunkFileNames: "[name].mjs", 14 | }, 15 | ], 16 | external: (id) => /node_modules/.test(id) || id.startsWith("react-just"), 17 | plugins: [typescript({ tsconfig: "tsconfig.build.json" }), nodeResolve()], 18 | }, 19 | { 20 | input: { handle: "src/handle.ts", vite: "src/vite.ts" }, 21 | output: [ 22 | { 23 | dir: "dist", 24 | format: "cjs", 25 | entryFileNames: "[name].cjs", 26 | chunkFileNames: "[name].cjs", 27 | }, 28 | ], 29 | external: (id) => /node_modules/.test(id) || id.startsWith("react-just"), 30 | plugins: [typescript({ tsconfig: "tsconfig.build.json" }), nodeResolve()], 31 | }, 32 | ]); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 almadoro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/Card.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 44 | 45 | 55 | -------------------------------------------------------------------------------- /docs/reference/router/use-pathname.md: -------------------------------------------------------------------------------- 1 | # usePathname 2 | 3 | `usePathname` is a hook that returns the current URL's pathname as a string. 4 | 5 | ```tsx 6 | "use client"; 7 | 8 | import { usePathname } from "@react-just/router"; 9 | 10 | function SomeComponent() { 11 | const pathname = usePathname(); 12 | 13 | // URL -> /profile/settings 14 | // `pathname` -> '/profile/settings' 15 | } 16 | ``` 17 | 18 | ## Parameters 19 | 20 | ```tsx 21 | const pathname = usePathname(); 22 | ``` 23 | 24 | `usePathname` takes no parameters. 25 | 26 | ## Returns 27 | 28 | `usePathname` returns the current URL pathname as a string. 29 | 30 | ```tsx 31 | function usePathname(): string; 32 | ``` 33 | 34 | The hook must be used within a [Router](/reference/router/router) component. Otherwise, it throws an error. 35 | 36 | ## Example 37 | 38 | | Route | URL | Returned value | 39 | | --------------- | -------------- | -------------- | 40 | | `/` | `/` | `"/"` | 41 | | `/orders` | `/orders` | `"/orders"` | 42 | | `/orders` | `/orders?id=2` | `"/orders"` | 43 | | `/blog/:blogId` | `/blog/1` | `"/blog/1"` | 44 | -------------------------------------------------------------------------------- /packages/vercel/types/handle.d.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import React, { ComponentType } from "react"; 3 | import { renderToPipeableStream as renderToPipeableHtmlStream } from "react-just/fizz.node"; 4 | import { 5 | createTemporaryReferenceSet, 6 | decodeAction, 7 | decodeFormState, 8 | decodeReply, 9 | renderToPipeableStream as renderToPipeableRscStream, 10 | runWithContext, 11 | } from "react-just/flight.node"; 12 | 13 | export interface HandleOptions { 14 | App: ComponentType; 15 | createTemporaryReferenceSet: typeof createTemporaryReferenceSet; 16 | decodeAction: typeof decodeAction; 17 | decodeFormState: typeof decodeFormState; 18 | decodeReply: typeof decodeReply; 19 | React: typeof React; 20 | renderToPipeableHtmlStream: typeof renderToPipeableHtmlStream; 21 | renderToPipeableRscStream: typeof renderToPipeableRscStream; 22 | resources: { 23 | css: string[]; 24 | js: string[]; 25 | }; 26 | runWithContext: typeof runWithContext; 27 | } 28 | 29 | export function createHandle(options: HandleOptions): HandleFunction; 30 | 31 | export type HandleFunction = (req: VercelRequest, res: VercelResponse) => void; 32 | -------------------------------------------------------------------------------- /packages/node/types/handle.d.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from "node:http"; 2 | import React, { ComponentType } from "react"; 3 | import { renderToPipeableStream as renderToPipeableHtmlStream } from "react-just/fizz.node"; 4 | import { 5 | createTemporaryReferenceSet, 6 | decodeAction, 7 | decodeFormState, 8 | decodeReply, 9 | renderToPipeableStream as renderToPipeableRscStream, 10 | runWithContext, 11 | } from "react-just/flight.node"; 12 | 13 | export interface HandleOptions { 14 | App: ComponentType; 15 | createTemporaryReferenceSet: typeof createTemporaryReferenceSet; 16 | decodeAction: typeof decodeAction; 17 | decodeFormState: typeof decodeFormState; 18 | decodeReply: typeof decodeReply; 19 | React: typeof React; 20 | renderToPipeableHtmlStream: typeof renderToPipeableHtmlStream; 21 | renderToPipeableRscStream: typeof renderToPipeableRscStream; 22 | resources: { 23 | css: string[]; 24 | js: string[]; 25 | }; 26 | runWithContext: typeof runWithContext; 27 | } 28 | 29 | export function createHandle(options: HandleOptions): HandleFunction; 30 | 31 | export type HandleFunction = ( 32 | req: IncomingMessage, 33 | res: ServerResponse, 34 | ) => void; 35 | -------------------------------------------------------------------------------- /packages/router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-just/router", 3 | "version": "0.2.0", 4 | "type": "module", 5 | "description": "Router for React Just", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/almadoro/react-just.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/almadoro/react-just/issues" 13 | }, 14 | "keywords": [ 15 | "router", 16 | "layout router", 17 | "react just" 18 | ], 19 | "files": [ 20 | "dist", 21 | "types" 22 | ], 23 | "exports": { 24 | ".": { 25 | "types": "./types/index.d.ts", 26 | "import": "./dist/index.mjs", 27 | "require": "./dist/index.cjs" 28 | } 29 | }, 30 | "scripts": { 31 | "build": "rm -rf dist && pnpm check && rollup -c", 32 | "check": "tsc --noEmit -p tsconfig.build.json" 33 | }, 34 | "devDependencies": { 35 | "@rollup/plugin-node-resolve": "^16.0.1", 36 | "@rollup/plugin-typescript": "^12.1.2", 37 | "@types/react": "^19.1.4", 38 | "react-just": "workspace:^", 39 | "rollup": "^4.52.2", 40 | "typescript": "^5.9.2" 41 | }, 42 | "peerDependencies": { 43 | "react-just": "workspace:^" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/router/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { LinkProps } from "@/types"; 4 | import { ReactElement, useCallback } from "react"; 5 | import useNavigate from "../hooks/use-navigate"; 6 | 7 | export default function Link({ 8 | href, 9 | replace, 10 | ...props 11 | }: LinkProps): ReactElement { 12 | const navigate = useNavigate(); 13 | 14 | const onClick: React.MouseEventHandler = useCallback( 15 | (e) => { 16 | props.onClick?.(e); 17 | 18 | if (e.defaultPrevented) return; 19 | 20 | const isExternal = e.currentTarget.origin !== window.location.origin; 21 | if (isExternal) return; 22 | 23 | if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; 24 | 25 | // Left button 26 | if (e.button !== 0) return; 27 | 28 | const target = e.currentTarget.getAttribute("target"); 29 | if (target && target !== "_self") return; 30 | 31 | if (e.currentTarget.getAttribute("download")) return; 32 | 33 | e.preventDefault(); 34 | 35 | navigate(href, { replace }); 36 | }, 37 | [href, replace, props.onClick, navigate], 38 | ); 39 | 40 | return ; 41 | } 42 | -------------------------------------------------------------------------------- /docs/guide/deploy.md: -------------------------------------------------------------------------------- 1 | # Deploy 2 | 3 | React Just is platform-agnostic, meaning it can be deployed to any environment that supports running JavaScript in some form. 4 | 5 | ## Official Platforms 6 | 7 | You can use prebuilt platform packages to deploy your app with minimal configuration. The following platforms are currently available or planned: 8 | 9 | | Platform | Status | Package | 10 | | ---------- | ---------------------------- | ----------------------------------------------- | 11 | | Node.js | :white_check_mark: Available | [`@react-just/node`](/guide/deploy/node.md) | 12 | | Vercel | :white_check_mark: Available | [`@react-just/vercel`](/guide/deploy/vercel.md) | 13 | | Netlify | :construction: Planned | `@react-just/netlify` | 14 | | Cloudflare | :construction: Planned | `@react-just/cloudflare` | 15 | 16 | ## Other Platforms 17 | 18 | Need support for a platform that isn’t available? 19 | 20 | You can [open an issue](https://github.com/almadoro/react-just/issues) with details about your use case. We'll provide guidance on implementation, and you're also welcome to contribute a platform package. 21 | -------------------------------------------------------------------------------- /packages/react-just/src/implementations.ts: -------------------------------------------------------------------------------- 1 | globalThis.__RJ_IMPL__ ||= {}; 2 | 3 | export const IMPLEMENTATION_EXPORT_NAME = "default"; 4 | 5 | export function getImplementation(id: string) { 6 | return globalThis.__RJ_IMPL__[id]?.[IMPLEMENTATION_EXPORT_NAME] ?? null; 7 | } 8 | 9 | export function registerImplementation(implementation: unknown, id: string) { 10 | globalThis.__RJ_IMPL__[id] = { [IMPLEMENTATION_EXPORT_NAME]: implementation }; 11 | } 12 | 13 | globalThis.__webpack_require__ = (id) => { 14 | const impl = globalThis.__RJ_IMPL__[id]; 15 | if (!impl) throw new Error(`Implementation for ${id} not found`); 16 | return impl; 17 | }; 18 | 19 | // We don't expect it to be used. 20 | globalThis.__webpack_chunk_load__ = (id) => { 21 | throw new Error( 22 | "__webpack_chunk_load__ is not supported. Trying to load implementation: " + 23 | id, 24 | ); 25 | }; 26 | 27 | declare global { 28 | var __RJ_IMPL__: Record; 29 | 30 | // react-server-dom-webpack expects __webpack_require__ and 31 | // __webpack_chunk_load__ to be available. 32 | 33 | function __webpack_require__(id: string): unknown; 34 | 35 | function __webpack_chunk_load__(id: string): never; 36 | } 37 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/index.ts: -------------------------------------------------------------------------------- 1 | import vitejsReact from "@vitejs/plugin-react"; 2 | import { PluginOption } from "vite"; 3 | import { ReactJustOptions } from "../../types/vite"; 4 | import build from "./build"; 5 | import clientHot from "./client-hot"; 6 | import css from "./css"; 7 | import entries, { 8 | CLIENT_ENTRY, 9 | FIZZ_ENTRY_NODE, 10 | FLIGHT_ENTRY_NODE, 11 | } from "./entries"; 12 | import environments, { 13 | ENVIRONMENTS as BASE_ENVIRONMENTS, 14 | } from "./environments"; 15 | import server from "./server"; 16 | import useClient from "./use-client"; 17 | import useServer from "./use-server"; 18 | 19 | export default function react(options?: ReactJustOptions): PluginOption { 20 | return [ 21 | useClient(), 22 | useServer(), 23 | vitejsReact(), 24 | environments(), 25 | entries({ app: options?.app }), 26 | css(), 27 | clientHot(), 28 | server(), 29 | build(), 30 | ]; 31 | } 32 | 33 | export const ENVIRONMENTS = { 34 | CLIENT: BASE_ENVIRONMENTS.CLIENT, 35 | FIZZ_NODE: BASE_ENVIRONMENTS.FIZZ_NODE, 36 | FLIGHT_NODE: BASE_ENVIRONMENTS.FLIGHT_NODE, 37 | }; 38 | 39 | export const ENTRIES = { 40 | CLIENT: CLIENT_ENTRY, 41 | FIZZ_NODE: FIZZ_ENTRY_NODE, 42 | FLIGHT_NODE: FLIGHT_ENTRY_NODE, 43 | }; 44 | -------------------------------------------------------------------------------- /packages/router/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from "@rollup/plugin-node-resolve"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | 5 | export default defineConfig([ 6 | { 7 | input: "src/index.ts", 8 | external: (id) => /node_modules/.test(id) || id.startsWith("react-just"), 9 | output: { 10 | preserveModules: true, 11 | preserveModulesRoot: "src", 12 | dir: "dist", 13 | format: "esm", 14 | entryFileNames: "[name].mjs", 15 | chunkFileNames: "[name]-[hash].mjs", 16 | }, 17 | plugins: [ 18 | typescript({ tsconfig: "tsconfig.build.json" }), 19 | nodeResolve(), 20 | { 21 | name: "preserve-use-client", 22 | transform(code) { 23 | if (/^("use client"|'use client');?$/m.test(code)) 24 | return { meta: { hasUseClient: true } }; 25 | }, 26 | renderChunk(code, chunk) { 27 | for (const id of chunk.moduleIds) { 28 | const { meta } = this.getModuleInfo(id); 29 | if (meta.hasUseClient) return `"use client";\n${code}`; 30 | } 31 | }, 32 | }, 33 | ], 34 | }, 35 | { 36 | input: "src/cjs.ts", 37 | output: { file: "dist/index.cjs", format: "cjs" }, 38 | }, 39 | ]); 40 | -------------------------------------------------------------------------------- /docs/reference/router/use-search-params.md: -------------------------------------------------------------------------------- 1 | # useSearchParams 2 | 3 | `useSearchParams` is a hook that returns the current URL’s query parameters as a [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object. 4 | 5 | ```tsx 6 | "use client"; 7 | 8 | import { useSearchParams } from "@react-just/router"; 9 | 10 | function SomeComponent() { 11 | const searchParams = useSearchParams(); 12 | 13 | const query = searchParams.get("q"); 14 | const type = searchParams.get("type"); 15 | 16 | // URL -> /search?q=duck&type=image 17 | // query -> "duck" 18 | // type -> "image" 19 | } 20 | ``` 21 | 22 | ## Parameters 23 | 24 | ```tsx 25 | const searchParams = useSearchParams(); 26 | ``` 27 | 28 | `useSearchParams` takes no parameters. 29 | 30 | ## Returns 31 | 32 | `useSearchParams` returns a Web [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object, which provides methods to read values from the URL's query string. 33 | 34 | ```tsx 35 | function useSearchParams(): URLSearchParams; 36 | ``` 37 | 38 | - The object is **read-only** in practice: calling modifying methods (like .set() or .delete()) does not update the URL or trigger a navigation. 39 | - The hook must be used within a [Router](/reference/router/router) component. Otherwise, it throws an error. 40 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/LevelIndicator.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 37 | 38 | 68 | -------------------------------------------------------------------------------- /packages/node/src/handle.ts: -------------------------------------------------------------------------------- 1 | import { HandleFunction, HandleOptions } from "@/types/handle"; 2 | import { createHandle as baseCreateHandle } from "react-just/handle.node"; 3 | 4 | export function createHandle(options: HandleOptions): HandleFunction { 5 | const { 6 | App, 7 | createTemporaryReferenceSet, 8 | decodeAction, 9 | decodeFormState, 10 | decodeReply, 11 | React, 12 | renderToPipeableHtmlStream, 13 | renderToPipeableRscStream, 14 | resources, 15 | runWithContext, 16 | } = options; 17 | 18 | const Root = () => 19 | React.createElement( 20 | React.Fragment, 21 | null, 22 | resources.js.map((src) => 23 | React.createElement("script", { 24 | key: src, 25 | src, 26 | async: true, 27 | }), 28 | ), 29 | resources.css.map((href) => 30 | React.createElement("link", { 31 | key: href, 32 | href, 33 | rel: "stylesheet", 34 | precedence: "default", 35 | }), 36 | ), 37 | React.createElement(App), 38 | ); 39 | 40 | return baseCreateHandle({ 41 | App: Root, 42 | createTemporaryReferenceSet, 43 | decodeAction, 44 | decodeFormState, 45 | decodeReply, 46 | React, 47 | renderToPipeableHtmlStream, 48 | renderToPipeableRscStream, 49 | runWithContext, 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /packages/vercel/src/handle.ts: -------------------------------------------------------------------------------- 1 | import { HandleFunction, HandleOptions } from "@/types/handle"; 2 | import { createHandle as baseCreateHandle } from "react-just/handle.node"; 3 | 4 | export function createHandle(options: HandleOptions): HandleFunction { 5 | const { 6 | App, 7 | createTemporaryReferenceSet, 8 | decodeAction, 9 | decodeFormState, 10 | decodeReply, 11 | React, 12 | renderToPipeableHtmlStream, 13 | renderToPipeableRscStream, 14 | resources, 15 | runWithContext, 16 | } = options; 17 | 18 | const Root = () => 19 | React.createElement( 20 | React.Fragment, 21 | null, 22 | resources.js.map((src) => 23 | React.createElement("script", { 24 | key: src, 25 | src, 26 | async: true, 27 | }), 28 | ), 29 | resources.css.map((href) => 30 | React.createElement("link", { 31 | key: href, 32 | href, 33 | rel: "stylesheet", 34 | precedence: "default", 35 | }), 36 | ), 37 | React.createElement(App), 38 | ); 39 | 40 | return baseCreateHandle({ 41 | App: Root, 42 | createTemporaryReferenceSet, 43 | decodeAction, 44 | decodeFormState, 45 | decodeReply, 46 | React, 47 | renderToPipeableHtmlStream, 48 | renderToPipeableRscStream, 49 | runWithContext, 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-client/directive.ts: -------------------------------------------------------------------------------- 1 | import { ExpressionStatement, Literal, Node, Program } from "estree"; 2 | 3 | const USE_CLIENT_DIRECTIVE = "use client"; 4 | 5 | /** 6 | * This is quick check to determine if the module could be a use client 7 | * module. Further validation must be done to ensure it's actually a use client 8 | * module with the `getIsUseClientModule` function. 9 | */ 10 | export function couldBeUseClientModule(code: string) { 11 | return /['"]use client['"]/.test(code); 12 | } 13 | 14 | export function getIsUseClientModule(program: Program) { 15 | return getUseClientDirective(program) !== null; 16 | } 17 | 18 | export function getUseClientDirective(program: Program) { 19 | // The "use client" directive is at program body level. 20 | // Can be after other directives but before any other type of node. 21 | 22 | for (const node of program.body) { 23 | if (isDirective(node) && node.expression.value === USE_CLIENT_DIRECTIVE) 24 | return node; 25 | 26 | if (node.type !== "ExpressionStatement") break; 27 | } 28 | 29 | return null; 30 | } 31 | 32 | function isDirective( 33 | node: Node, 34 | ): node is ExpressionStatement & { expression: Literal } { 35 | return ( 36 | node.type === "ExpressionStatement" && 37 | node.expression.type === "Literal" && 38 | typeof node.expression.value === "string" 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-just/types/shared.d.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from "node:stream"; 2 | import { ReactNode } from "react"; 3 | 4 | export interface JustRequest { 5 | readonly headers: Headers; 6 | readonly method: string; 7 | readonly url: string; 8 | } 9 | 10 | export interface JustResponse { 11 | readonly headers: Headers; 12 | } 13 | 14 | export type PipeableStream = { 15 | abort(reason: unknown): void; 16 | pipe(destination: T): T; 17 | }; 18 | 19 | export type RscPayload = { 20 | formState: ReactFormState | null; 21 | tree: ReactNode; 22 | }; 23 | 24 | export type ReactFormState = [ReactClientValue, string, string, number]; 25 | 26 | // Serializable values 27 | export type ReactClientValue = 28 | | ReactNode 29 | | string 30 | | boolean 31 | | number 32 | | symbol 33 | | null 34 | | void 35 | | bigint 36 | | ReadableStream 37 | | AsyncIterable 38 | | AsyncIterator 39 | | Iterable 40 | | Iterator 41 | | Array 42 | | Map 43 | | Set 44 | | FormData 45 | | ArrayBufferView 46 | | ArrayBuffer 47 | | Date 48 | | ReactClientObject 49 | | Promise; // Thenable 50 | 51 | type ReactClientObject = { [key: string]: ReactClientValue }; 52 | -------------------------------------------------------------------------------- /docs/guide/request-and-response.md: -------------------------------------------------------------------------------- 1 | # Request and Response 2 | 3 | You can use [`request`](/reference/core/server#request) inside **Server Components**, **Server Functions**, or their dependencies to access request details during server rendering or while running a Server Function. 4 | 5 | ```tsx [src/Profile.tsx] 6 | import { request } from "react-just/server"; 7 | 8 | function Profile() { 9 | const { url } = request(); 10 | 11 | // Use the URL to render the selected profile tab 12 | const tab = getSelectedTab(url); 13 | } 14 | ``` 15 | 16 | ```ts [src/api.ts] 17 | "use server"; 18 | 19 | import { request } from "react-just/server"; 20 | 21 | export async function editPreferences() { 22 | const { headers } = request(); 23 | 24 | // Use the cookie header to read the session 25 | const session = await getSession(headers.get("cookie")); 26 | } 27 | ``` 28 | 29 | You can use [`response`](/reference/core/server#response) inside **Server Functions** or their dependencies to modify the HTTP response. 30 | 31 | ```ts [src/actions.ts] 32 | "use server"; 33 | 34 | import { response } from "react-just/server"; 35 | 36 | export async function saveSettings() { 37 | const { headers } = response(); 38 | 39 | // Set a cookie with rendering preferences 40 | headers.append("Set-Cookie", getThemeSetCookie()); 41 | } 42 | ``` 43 | 44 | Visit the [Server Utilities reference](/reference/core/server) for more usage details. 45 | -------------------------------------------------------------------------------- /templates/node-js/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | color: #ffffff; 4 | background-color: #16181d; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | display: flex; 10 | place-items: center; 11 | min-height: 100vh; 12 | } 13 | 14 | h1 { 15 | font-size: 3.2em; 16 | line-height: 1.1; 17 | } 18 | 19 | a { 20 | font-weight: 500; 21 | color: #00a67d; 22 | } 23 | a:hover { 24 | color: #00bc8d; 25 | } 26 | 27 | .app { 28 | max-width: 1000px; 29 | margin: 0 auto; 30 | padding: 16px; 31 | text-align: center; 32 | } 33 | 34 | .react-just-logo { 35 | animation: spin 8s linear infinite; 36 | width: 140px; 37 | height: 140px; 38 | margin-top: 16px; 39 | margin-bottom: 32px; 40 | } 41 | 42 | @keyframes spin { 43 | from { 44 | transform: rotate(0deg); 45 | } 46 | to { 47 | transform: rotate(360deg); 48 | } 49 | } 50 | 51 | .vite-section p { 52 | color: #999; 53 | font-size: 0.9rem; 54 | } 55 | 56 | .vite-logo { 57 | width: 30px; 58 | height: 30px; 59 | } 60 | 61 | .counters-container { 62 | display: grid; 63 | grid-template-columns: 1fr; 64 | gap: 12px; 65 | max-width: 540px; 66 | margin-bottom: 24px; 67 | } 68 | 69 | @media (min-width: 600px) { 70 | .counters-container { 71 | grid-template-columns: 1fr 1fr; 72 | } 73 | } 74 | 75 | .counters-container p { 76 | font-size: 0.85rem; 77 | color: #999; 78 | } 79 | -------------------------------------------------------------------------------- /templates/node-ts/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | color: #ffffff; 4 | background-color: #16181d; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | display: flex; 10 | place-items: center; 11 | min-height: 100vh; 12 | } 13 | 14 | h1 { 15 | font-size: 3.2em; 16 | line-height: 1.1; 17 | } 18 | 19 | a { 20 | font-weight: 500; 21 | color: #00a67d; 22 | } 23 | a:hover { 24 | color: #00bc8d; 25 | } 26 | 27 | .app { 28 | max-width: 1000px; 29 | margin: 0 auto; 30 | padding: 16px; 31 | text-align: center; 32 | } 33 | 34 | .react-just-logo { 35 | animation: spin 8s linear infinite; 36 | width: 140px; 37 | height: 140px; 38 | margin-top: 16px; 39 | margin-bottom: 32px; 40 | } 41 | 42 | @keyframes spin { 43 | from { 44 | transform: rotate(0deg); 45 | } 46 | to { 47 | transform: rotate(360deg); 48 | } 49 | } 50 | 51 | .vite-section p { 52 | color: #999; 53 | font-size: 0.9rem; 54 | } 55 | 56 | .vite-logo { 57 | width: 30px; 58 | height: 30px; 59 | } 60 | 61 | .counters-container { 62 | display: grid; 63 | grid-template-columns: 1fr; 64 | gap: 12px; 65 | max-width: 540px; 66 | margin-bottom: 24px; 67 | } 68 | 69 | @media (min-width: 600px) { 70 | .counters-container { 71 | grid-template-columns: 1fr 1fr; 72 | } 73 | } 74 | 75 | .counters-container p { 76 | font-size: 0.85rem; 77 | color: #999; 78 | } 79 | -------------------------------------------------------------------------------- /templates/vercel-ts/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | color: #ffffff; 4 | background-color: #16181d; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | display: flex; 10 | place-items: center; 11 | min-height: 100vh; 12 | } 13 | 14 | h1 { 15 | font-size: 3.2em; 16 | line-height: 1.1; 17 | } 18 | 19 | a { 20 | font-weight: 500; 21 | color: #00a67d; 22 | } 23 | a:hover { 24 | color: #00bc8d; 25 | } 26 | 27 | .app { 28 | max-width: 1000px; 29 | margin: 0 auto; 30 | padding: 16px; 31 | text-align: center; 32 | } 33 | 34 | .react-just-logo { 35 | animation: spin 8s linear infinite; 36 | width: 140px; 37 | height: 140px; 38 | margin-top: 16px; 39 | margin-bottom: 32px; 40 | } 41 | 42 | @keyframes spin { 43 | from { 44 | transform: rotate(0deg); 45 | } 46 | to { 47 | transform: rotate(360deg); 48 | } 49 | } 50 | 51 | .vite-section p { 52 | color: #999; 53 | font-size: 0.9rem; 54 | } 55 | 56 | .vite-logo { 57 | width: 30px; 58 | height: 30px; 59 | } 60 | 61 | .counters-container { 62 | display: grid; 63 | grid-template-columns: 1fr; 64 | gap: 12px; 65 | max-width: 540px; 66 | margin-bottom: 24px; 67 | } 68 | 69 | @media (min-width: 600px) { 70 | .counters-container { 71 | grid-template-columns: 1fr 1fr; 72 | } 73 | } 74 | 75 | .counters-container p { 76 | font-size: 0.85rem; 77 | color: #999; 78 | } 79 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-client/transform/export-named-specifiers.ts: -------------------------------------------------------------------------------- 1 | import { ExportNamedDeclaration } from "estree"; 2 | import Generator from "./generator"; 3 | import Module from "./module"; 4 | 5 | /** 6 | * Transforms exports in the form of: 7 | * ```ts 8 | * let a; 9 | * let b; 10 | * let d; 11 | * export { a, b as c, d as default }; 12 | * ``` 13 | * 14 | * Into: 15 | * ```ts 16 | * let a; 17 | * let b; 18 | * let d; 19 | * const $$Ref$$a = $$registerClientReference$$(...); 20 | * const $$Ref$$c = $$registerClientReference$$(...); 21 | * const $$Ref$$default = $$registerClientReference$$(...); 22 | * export { $$Ref$$a as a, $$Ref$$c as c, $$Ref$$default as default }; 23 | * ``` 24 | */ 25 | export default function transformExportNamedSpecifiers( 26 | node: ExportNamedDeclaration, 27 | module: Module, 28 | generator: Generator, 29 | ) { 30 | for (const specifier of node.specifiers) { 31 | if ( 32 | specifier.local.type !== "Identifier" || 33 | specifier.exported.type !== "Identifier" 34 | ) 35 | /* c8 ignore next */ 36 | continue; 37 | 38 | const localIdentifier = specifier.local.name; 39 | const exportIdentifier = specifier.exported.name; 40 | 41 | module.append( 42 | ...generator.createRegisterAndExportReference( 43 | exportIdentifier, 44 | localIdentifier, 45 | ), 46 | ); 47 | } 48 | 49 | module.remove(node); 50 | } 51 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-server/transform/export-named-specifiers.ts: -------------------------------------------------------------------------------- 1 | import { ExportNamedDeclaration } from "estree"; 2 | import Generator from "./generator"; 3 | import Module from "./module"; 4 | 5 | /** 6 | * Transforms exports in the form of: 7 | * ```ts 8 | * let a; 9 | * let b; 10 | * let d; 11 | * export { a, b as c, d as default }; 12 | * ``` 13 | * 14 | * Into: 15 | * ```ts 16 | * let a; 17 | * let b; 18 | * let d; 19 | * const $$Ref$$a = $$registerServerReference$$(...); 20 | * const $$Ref$$c = $$registerServerReference$$(...); 21 | * const $$Ref$$default = $$registerServerReference$$(...); 22 | * export { $$Ref$$a as a, $$Ref$$c as c, $$Ref$$default as default }; 23 | * ``` 24 | */ 25 | export default function transformExportNamedSpecifiers( 26 | node: ExportNamedDeclaration, 27 | module: Module, 28 | generator: Generator, 29 | ) { 30 | for (const specifier of node.specifiers) { 31 | if ( 32 | specifier.local.type !== "Identifier" || 33 | specifier.exported.type !== "Identifier" 34 | ) 35 | /* c8 ignore next */ 36 | continue; 37 | 38 | const localIdentifier = specifier.local.name; 39 | const exportIdentifier = specifier.exported.name; 40 | 41 | module.append( 42 | ...generator.createRegisterAndExportReference( 43 | exportIdentifier, 44 | localIdentifier, 45 | ), 46 | ); 47 | } 48 | 49 | module.remove(node); 50 | } 51 | -------------------------------------------------------------------------------- /packages/vercel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-just/vercel", 3 | "version": "0.3.0", 4 | "type": "module", 5 | "description": "Official Vercel adapter for React Just", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/almadoro/react-just.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/almadoro/react-just/issues" 13 | }, 14 | "keywords": [ 15 | "vercel", 16 | "react just" 17 | ], 18 | "files": [ 19 | "dist", 20 | "types" 21 | ], 22 | "exports": { 23 | ".": { 24 | "types": "./types/vite.d.ts", 25 | "import": "./dist/vite.mjs", 26 | "require": "./dist/vite.cjs" 27 | }, 28 | "./handle": { 29 | "types": "./types/handle.d.ts", 30 | "import": "./dist/handle.mjs", 31 | "require": "./dist/handle.cjs" 32 | } 33 | }, 34 | "scripts": { 35 | "build": "rm -rf dist && pnpm check && rollup -c", 36 | "check": "tsc --noEmit -p tsconfig.build.json" 37 | }, 38 | "devDependencies": { 39 | "@rollup/plugin-node-resolve": "^16.0.1", 40 | "@rollup/plugin-typescript": "^12.1.1", 41 | "@types/node": "^24.3.3", 42 | "@types/react": "^19.1.8", 43 | "@vercel/node": "^5.3.22", 44 | "react-just": "workspace:^", 45 | "rollup": "^4.50.1", 46 | "typescript": "^5.9.2", 47 | "vite": "^7.0.6" 48 | }, 49 | "peerDependencies": { 50 | "react-just": "workspace:^" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/reference/router/route-component-props.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2, 3] 3 | --- 4 | 5 | # RouteComponentProps 6 | 7 | `RouteComponentProps` is a TypeScript interface that defines the props automatically passed to components used as the `component` prop on [Route](/reference/router/route) elements. 8 | 9 | ```tsx 10 | import type { RouteComponentProps } from "@react-just/router"; 11 | 12 | interface UserParams { 13 | id: string; 14 | } 15 | 16 | export default function UserProfile({ 17 | params, 18 | pathname, 19 | search, 20 | url, 21 | }: RouteComponentProps) { 22 | return ( 23 |
24 |

User Profile

25 |

User ID: {params.id}

26 |

Current path: {pathname}

27 |

Search: {search}

28 |
29 | ); 30 | } 31 | ``` 32 | 33 | ## Definition 34 | 35 | ```tsx 36 | interface RouteComponentProps { 37 | children?: ReactNode; 38 | params: T; 39 | pathname: string; 40 | search: string; 41 | url: string; 42 | } 43 | 44 | type Params = Record; 45 | ``` 46 | 47 | Where: 48 | 49 | - `children`: Child components passed through nested routing. 50 | - `params`: Object containing extracted parameters. 51 | - `pathname`: The current URL pathname (e.g., `/users/123`). 52 | - `search`: The current URL search string (e.g., `?tab=profile&sort=name`). Can be parsed with `URLSearchParams`. 53 | - `url`: The complete URL as a string. 54 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/utils.ts: -------------------------------------------------------------------------------- 1 | import { DevEnvironment, EnvironmentModuleNode } from "vite"; 2 | 3 | // Vite will use query params like `?v=` sometimes. 4 | export function cleanId(id: string) { 5 | return id.split("?")[0]; 6 | } 7 | 8 | export async function optimizeDeps(env: DevEnvironment) { 9 | const { depsOptimizer } = env; 10 | 11 | if (!depsOptimizer) 12 | throw new Error(`Deps optimizer not found on ${env.name} environment`); 13 | 14 | const initialMetadata = depsOptimizer.metadata; 15 | 16 | depsOptimizer.run(); 17 | 18 | // Vite doesn't provide a way to wait for the optimization to be done. 19 | // We need to poll the metadata instance until it changes. 20 | // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/optimizer/optimizer.ts#L419 21 | await new Promise((resolve) => { 22 | const interval = setInterval(() => { 23 | if (depsOptimizer.metadata === initialMetadata) return; 24 | clearInterval(interval); 25 | resolve(); 26 | }, 0); 27 | }); 28 | } 29 | 30 | export function invalidateModules(env: DevEnvironment, ...ids: string[]) { 31 | const invalidatedModules = new Set(); 32 | for (const moduleId of ids) { 33 | const module = env.moduleGraph.getModuleById(moduleId); 34 | if (module) 35 | env.moduleGraph.invalidateModule( 36 | module, 37 | invalidatedModules, 38 | Date.now(), 39 | true, 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/Home.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 40 | 41 | 52 | -------------------------------------------------------------------------------- /docs/guide/client-and-server-components.md: -------------------------------------------------------------------------------- 1 | # Client and Server Components 2 | 3 | ::: info 4 | From the [React documentation](https://react.dev/reference/rsc/use-client#caveats): "A component usage is considered a **Client Component** if it is defined in module with `use client` directive or when it is a transitive dependency of a module that contains a `use client` directive. Otherwise, it is a **Server Component**." 5 | ::: 6 | 7 | Only the [App Component](/guide/app-component) must be a Server Component. Other components (and modules) can run on the server, the client, or both. For example, the following `Counter` component is a Client Component: 8 | 9 | ```tsx [src/Counter.tsx] {1} 10 | "use client"; 11 | 12 | import React, { useState } from "react"; 13 | 14 | export default function Counter() { 15 | const [count, setCount] = useState(0); 16 | 17 | return ( 18 | 21 | ); 22 | } 23 | ``` 24 | 25 | And the App Component is a Server Component. 26 | 27 | ```tsx [src/index.tsx] {1,8} 28 | import Counter from "./Counter"; 29 | 30 | export default async function App() { 31 | return ( 32 | 33 | 34 |

Hello from the server

35 | 36 | 37 | 38 | ); 39 | } 40 | ``` 41 | 42 | To learn more about Server Components, see the [React documentation](https://react.dev/reference/rsc/server-components). 43 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/build.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "vite"; 2 | import { SCAN_USE_CLIENT_ENTRY, SCAN_USE_SERVER_ENTRY } from "./entries"; 3 | import { ENVIRONMENTS } from "./environments"; 4 | 5 | export default function build(): Plugin { 6 | return { 7 | name: "react-just:build", 8 | apply: "build", 9 | async buildApp(builder) { 10 | // The use client scan must be executed first because the use server scan 11 | // depends on a dynamic module that changes with the use client scan. 12 | await builder.build( 13 | builder.environments[ENVIRONMENTS.SCAN_USE_CLIENT_MODULES], 14 | ); 15 | await builder.build( 16 | builder.environments[ENVIRONMENTS.SCAN_USE_SERVER_MODULES], 17 | ); 18 | }, 19 | config() { 20 | return { 21 | appType: "custom", 22 | // React expects this variable to be replaced during build time. 23 | define: { "process.env.NODE_ENV": JSON.stringify("production") }, 24 | environments: { 25 | [ENVIRONMENTS.SCAN_USE_CLIENT_MODULES]: { 26 | build: { 27 | rollupOptions: { input: SCAN_USE_CLIENT_ENTRY }, 28 | write: false, 29 | }, 30 | }, 31 | [ENVIRONMENTS.SCAN_USE_SERVER_MODULES]: { 32 | build: { 33 | rollupOptions: { input: SCAN_USE_SERVER_ENTRY }, 34 | write: false, 35 | }, 36 | }, 37 | }, 38 | }; 39 | }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/router/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType, ReactElement, ReactNode } from "react"; 2 | 3 | export interface Navigate { 4 | (href: string, options?: NavigateOptions): void; 5 | (delta: number, options?: never): void; 6 | } 7 | 8 | export interface NavigateOptions { 9 | replace?: boolean; 10 | } 11 | 12 | export type Params = Record; 13 | 14 | export function Link(props: LinkProps): ReactElement; 15 | 16 | export interface LinkProps extends React.DOMAttributes { 17 | href: string; 18 | replace?: boolean; 19 | } 20 | 21 | export interface RouteProps { 22 | children?: RouteChildren; 23 | component: ComponentType; 24 | path: string; 25 | } 26 | 27 | export function Route(props: RouteProps): never; 28 | 29 | export interface RouterProps { 30 | children?: RouteChildren; 31 | url: URL; 32 | } 33 | 34 | export function Router(props: RouterProps): ReactNode; 35 | 36 | export type RouteChildren = 37 | | Iterable 38 | | ReactElement 39 | | boolean 40 | | null 41 | | undefined; 42 | 43 | export interface RouteComponentProps { 44 | children?: ReactNode; 45 | params: T; 46 | pathname: string; 47 | search: string; 48 | url: string; 49 | } 50 | 51 | export function useNavigate(): Navigate; 52 | 53 | export function useParams(): T; 54 | 55 | export function usePathname(): string; 56 | 57 | export function useSearchParams(): URLSearchParams; 58 | -------------------------------------------------------------------------------- /docs/guide/deploy/vercel.md: -------------------------------------------------------------------------------- 1 | # Vercel Deployment 2 | 3 | Deploy your React Just app on [Vercel](https://vercel.com/). 4 | 5 | ## Installation 6 | 7 | Install the Vercel adapter package in your project: 8 | 9 | ::: code-group 10 | 11 | ```bash [npm] 12 | $ npm install @react-just/vercel 13 | ``` 14 | 15 | ```bash [pnpm] 16 | $ pnpm add @react-just/vercel 17 | ``` 18 | 19 | ```bash [bun] 20 | $ bun add @react-just/vercel 21 | ``` 22 | 23 | ::: 24 | 25 | Add the plugin in the Vite config file: 26 | 27 | ```ts [vite.config.ts] {1,6} 28 | import vercel from "@react-just/vercel"; 29 | import react from "react-just/vite"; 30 | import { defineConfig } from "vite"; 31 | 32 | export default defineConfig({ 33 | plugins: [react(), vercel()], 34 | }); 35 | ``` 36 | 37 | ## Building the App 38 | 39 | Build the app with the `vite build` command. For convenience, add the following script to your `package.json`: 40 | 41 | ```json [package.json] {3} 42 | { 43 | "scripts": { 44 | "build": "vite build" 45 | } 46 | } 47 | ``` 48 | 49 | Then build the app with: 50 | 51 | ::: code-group 52 | 53 | ```bash [npm] 54 | $ npm run build 55 | ``` 56 | 57 | ```bash [pnpm] 58 | $ pnpm build 59 | ``` 60 | 61 | ```bash [bun] 62 | $ bun run build 63 | ``` 64 | 65 | ::: 66 | 67 | The build process generates a `.vercel/output` directory following Vercel's [Build Output API](https://vercel.com/docs/build-output-api) specification. 68 | 69 | For details about the adapter and its output format, see the [Vercel adapter reference](/reference/platforms/vercel). 70 | -------------------------------------------------------------------------------- /packages/react-just/types/flight.node.d.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from "node:http"; 2 | import { 3 | JustRequest, 4 | JustResponse, 5 | PipeableStream, 6 | ReactClientValue, 7 | ReactFormState, 8 | } from "./shared"; 9 | 10 | export function createTemporaryReferenceSet(): TemporaryReferenceSet; 11 | 12 | export function decodeAction(body: FormData): Promise<() => T> | null; 13 | 14 | export function decodeFormState( 15 | actionResult: S, 16 | body: FormData, 17 | ): Promise; 18 | 19 | export function decodeReply( 20 | req: IncomingMessage, 21 | options: DecodeReplyOptions, 22 | ): Promise; 23 | 24 | export function registerClientReference( 25 | moduleId: string | number, 26 | exportName: string | number, 27 | ): unknown; 28 | 29 | export function registerServerReference( 30 | reference: T, 31 | id: string, 32 | ): T; 33 | 34 | export function renderToPipeableStream( 35 | value: ReactClientValue, 36 | options: RenderToPipeableStreamOptions, 37 | ): PipeableStream; 38 | 39 | export function runWithContext(context: Context, fn: () => void): Promise; 40 | 41 | export interface Context { 42 | req: JustRequest; 43 | res: JustResponse; 44 | } 45 | 46 | export interface DecodeReplyOptions { 47 | temporaryReferences: TemporaryReferenceSet; 48 | } 49 | 50 | export interface RenderToPipeableStreamOptions { 51 | temporaryReferences: TemporaryReferenceSet; 52 | } 53 | 54 | export type TemporaryReferenceSet = WeakMap; 55 | 56 | interface TemporaryReference {} 57 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/icons/react-router-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /templates/node-js/src/assets/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/node-ts/src/assets/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/icons/react-router-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-just/node", 3 | "version": "0.4.0", 4 | "type": "module", 5 | "description": "Official Node.js adapter for React Just", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/almadoro/react-just.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/almadoro/react-just/issues" 13 | }, 14 | "keywords": [ 15 | "node", 16 | "nodejs", 17 | "react just" 18 | ], 19 | "files": [ 20 | "dist", 21 | "types" 22 | ], 23 | "bin": { 24 | "react-just-node": "dist/bin.mjs" 25 | }, 26 | "exports": { 27 | ".": { 28 | "types": "./types/vite.d.ts", 29 | "import": "./dist/vite.mjs", 30 | "require": "./dist/vite.cjs" 31 | }, 32 | "./handle": { 33 | "types": "./types/handle.d.ts", 34 | "import": "./dist/handle.mjs", 35 | "require": "./dist/handle.cjs" 36 | } 37 | }, 38 | "scripts": { 39 | "build": "rm -rf dist && pnpm check && rollup -c", 40 | "check": "tsc --noEmit -p tsconfig.build.json" 41 | }, 42 | "dependencies": { 43 | "commander": "^14.0.0", 44 | "mime": "^4.0.7" 45 | }, 46 | "devDependencies": { 47 | "@rollup/plugin-node-resolve": "^16.0.1", 48 | "@rollup/plugin-typescript": "^12.1.2", 49 | "@types/node": "^22.15.17", 50 | "@types/react": "^19.1.4", 51 | "react-just": "workspace:^", 52 | "rollup": "^4.41.0", 53 | "typescript": "^5.9.2", 54 | "vite": "^7.0.6" 55 | }, 56 | "peerDependencies": { 57 | "react-just": "workspace:^" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /templates/vercel-ts/src/assets/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/guide/tailwindcss.md: -------------------------------------------------------------------------------- 1 | # Tailwind CSS 2 | 3 | Integrate Tailwind with your React Just app. 4 | 5 | ## Installation 6 | 7 | Install Tailwind CSS and its Vite plugin in your project: 8 | 9 | ::: code-group 10 | 11 | ```bash [npm] 12 | $ npm install tailwindcss @tailwindcss/vite 13 | ``` 14 | 15 | ```bash [pnpm] 16 | $ pnpm add tailwindcss @tailwindcss/vite 17 | ``` 18 | 19 | ```bash [bun] 20 | $ bun add tailwindcss @tailwindcss/vite 21 | ``` 22 | 23 | ::: 24 | 25 | ## Configuration 26 | 27 | Ensure your `package.json` has `type: "module"` specified to be able to import the Tailwind CSS plugin: 28 | 29 | ```json [package.json] {2} 30 | { 31 | "type": "module" 32 | } 33 | ``` 34 | 35 | Add the Tailwind plugin to your Vite config file: 36 | 37 | ```ts [vite.config.ts] {1,6} 38 | import tailwindcss from "@tailwindcss/vite"; 39 | import react from "react-just/vite"; 40 | import { defineConfig } from "vite"; 41 | 42 | export default defineConfig({ 43 | plugins: [react(), tailwindcss()], 44 | }); 45 | ``` 46 | 47 | ## Setup Styles 48 | 49 | Create a CSS module (e.g. `src/index.css`) and import Tailwind CSS: 50 | 51 | ```css [src/index.css] 52 | @import "tailwindcss"; 53 | ``` 54 | 55 | Import the CSS module in your App Component: 56 | 57 | ```tsx [src/index.tsx] {1} 58 | import "./index.css"; 59 | 60 | export default function App() { 61 | return ( 62 | 63 | 64 |

65 | Hello React Just with Tailwind! 66 |

67 | 68 | 69 | ); 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/reference/router.md: -------------------------------------------------------------------------------- 1 | # Router (`@react-just/router`) 2 | 3 | ## Components 4 | 5 |
6 | 7 | 8 | 9 |
10 | 11 | ## Hooks 12 | 13 |
14 | 15 | 16 | 17 | 18 |
19 | 20 | ## Utilities 21 | 22 |
23 | 24 |
25 | 26 | 40 | -------------------------------------------------------------------------------- /docs/reference/router/use-params.md: -------------------------------------------------------------------------------- 1 | # useParams 2 | 3 | `useParams` is a hook that returns the route parameters extracted from the current URL. 4 | 5 | ```tsx 6 | "use client"; 7 | 8 | import { useParams } from "@react-just/router"; 9 | 10 | function SomeComponent() { 11 | const params = useParams(); 12 | 13 | // Route -> /items/:category/:itemId 14 | // URL -> /items/shoes/1023 15 | // `params` -> { category: "shoes", itemId: "1023" } 16 | } 17 | ``` 18 | 19 | ## Parameters 20 | 21 | ```tsx 22 | const params = useParams(); 23 | ``` 24 | 25 | `useParams` accepts an _optional_ generic type parameter `TReturn`, which overrides the return type. By default, the return type is an empty object `{}`. 26 | 27 | ## Returns 28 | 29 | `useParams` returns an object with the parameters from the currently matched route. 30 | 31 | ```tsx 32 | function useParams(): TReturn; 33 | 34 | type Params = Record; 35 | ``` 36 | 37 | - Each property corresponds to a parameter in the active route. 38 | - The property value is a string for path parameters, or an array of strings for wildcard parameters. 39 | - The hook must be used within a [Router](/reference/router/router) component. Otherwise, it throws an error. 40 | 41 | ## Examples 42 | 43 | | Route | URL | useParams() | 44 | | ------------------- | ------------ | ------------------------- | 45 | | `/items` | `/items` | `{}` | 46 | | `/items/:id` | `/items/1` | `{ id: "1" }` | 47 | | `/items/:tag/:item` | `/items/1/2` | `{ tag: "1", item: "2" }` | 48 | | `/items/*rest` | `/items/1/2` | `{ rest: ["1", "2"] }` | 49 | -------------------------------------------------------------------------------- /docs/reference/platforms/node.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2, 3] 3 | --- 4 | 5 | # Node.js Adapter (`@react-just/node`) 6 | 7 | ## Plugin Usage 8 | 9 | ```ts [vite.config.ts] {1,6} 10 | import node from "@react-just/node"; 11 | import react from "react-just/vite"; 12 | import { defineConfig } from "vite"; 13 | 14 | export default defineConfig({ 15 | plugins: [react(), node()], 16 | }); 17 | ``` 18 | 19 | ## CLI Usage 20 | 21 | The Node.js adapter provides a CLI command to serve your React Just app. 22 | 23 | ::: code-group 24 | 25 | ```bash [npm] 26 | $ npx react-just-node [build] -p [port] [--no-static] 27 | ``` 28 | 29 | ```bash [pnpm] 30 | $ pnpm react-just-node [build] -p [port] [--no-static] 31 | ``` 32 | 33 | ```bash [bun] 34 | $ bun react-just-node [build] -p [port] [--no-static] 35 | ``` 36 | 37 | ::: 38 | 39 | ### Parameters 40 | 41 | - `build` (optional): Path to the build output folder. Defaults to `dist`. 42 | - `-p` or `--port` (optional): Port to run the server on. Defaults to `3000`. 43 | - `--no-static` (optional): Disables serving static files from the `static` directory. 44 | 45 | --- 46 | 47 | ### Examples 48 | 49 | Serve a custom build folder `output` on port `4000`: 50 | 51 | ::: code-group 52 | 53 | ```bash [npm] 54 | $ npx react-just-node output -p 4000 55 | ``` 56 | 57 | ```bash [pnpm] 58 | $ pnpm react-just-node output -p 4000 59 | ``` 60 | 61 | ```bash [bun] 62 | $ bun react-just-node output -p 4000 63 | ``` 64 | 65 | ::: 66 | 67 | Run without serving static files: 68 | 69 | ::: code-group 70 | 71 | ```bash [npm] 72 | $ npx react-just-node --no-static 73 | ``` 74 | 75 | ```bash [pnpm] 76 | $ pnpm react-just-node --no-static 77 | ``` 78 | 79 | ```bash [bun] 80 | $ bun react-just-node --no-static 81 | ``` 82 | 83 | ::: 84 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## What is React Just? 4 | 5 | React Just is the simplest way to use React with React Server Components (Server Components and Server Functions). 6 | 7 | ## Pre-requisite knowledge 8 | 9 | This documentation assumes you're somewhat familiar with React, [Server Components](https://react.dev/reference/rsc/server-components) and [Server Functions](https://react.dev/reference/rsc/server-functions). 10 | 11 | ## Try It Out Online 12 | 13 | You can try the JavaScript version below, but we recommend opening it in a separate tab via the [JavaScript](https://stackblitz.com/github/almadoro/react-just/tree/main/templates/node-js?file=src%2Findex.jsx&startScript=dev) or [TypeScript](https://stackblitz.com/github/almadoro/react-just/tree/main/templates/node-ts?file=src%2Findex.tsx&startScript=dev) links. 14 | 15 | 16 | 17 | ::: warning `start` command 18 | The `start` command may not work on StackBlitz, but it works correctly on a Node.js environment. 19 | ::: 20 | 21 | ## Why React Just? 22 | 23 | React Server Components are a specification designed for full-stack React frameworks. Frameworks like **Next.js** and **React Router** implement this specification, but they also add additional features and conventions that often make using **full-stack React more complex than it needs to be**. 24 | 25 | **React Just takes a different approach**: it focuses only on the RSC specification. No opinions, no extra conventions, no hidden features, just React Server Components in the simplest possible form. This keeps applications **easy to understand, easy to mantain and easy to deploy anywhere**. 26 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-server/directive.ts: -------------------------------------------------------------------------------- 1 | import { ExpressionStatement, Literal, Node, Program } from "estree"; 2 | import { traverse } from "estree-toolkit"; 3 | 4 | const USE_SERVER_DIRECTIVE = "use server"; 5 | 6 | /** 7 | * This is quick check to determine if the module could contain the 8 | * "use server" directive. Further validation must be done to ensure 9 | * it actually contains the directive. 10 | */ 11 | export function couldContainUseServerDirective(code: string) { 12 | return /['"]use server['"]/.test(code); 13 | } 14 | 15 | export function getIsUseServerDirective(node: Node) { 16 | return isDirective(node) && node.expression.value === USE_SERVER_DIRECTIVE; 17 | } 18 | 19 | export function getUseServerDirectiveScope(program: Program) { 20 | let contains = false; 21 | 22 | traverse(program, { 23 | ExpressionStatement(path) { 24 | if (path.node && getIsUseServerDirective(path.node)) { 25 | contains = true; 26 | } 27 | }, 28 | }); 29 | 30 | if (!contains) return null; 31 | 32 | if (getUseServerModuleDirective(program)) return "module"; 33 | 34 | return "function"; 35 | } 36 | 37 | export function getUseServerModuleDirective(program: Program) { 38 | // The "use server" directive is at program body level. 39 | // Can be after other directives but before any other type of node. 40 | for (const node of program.body) { 41 | if (getIsUseServerDirective(node)) return node; 42 | 43 | if (node.type !== "ExpressionStatement") break; 44 | } 45 | 46 | return null; 47 | } 48 | 49 | function isDirective( 50 | node: Node, 51 | ): node is ExpressionStatement & { expression: Literal } { 52 | return ( 53 | node.type === "ExpressionStatement" && 54 | node.expression.type === "Literal" && 55 | typeof node.expression.value === "string" 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/directive.test.ts: -------------------------------------------------------------------------------- 1 | import transform, { TransformOptions } from "@/vite/use-client/transform"; 2 | import Generator from "@/vite/use-client/transform/generator"; 3 | import { generate } from "astring"; 4 | import { builders } from "estree-toolkit"; 5 | import fs from "node:fs/promises"; 6 | import path from "node:path"; 7 | import { parseAstAsync } from "vite"; 8 | import { describe, expect, test } from "vitest"; 9 | 10 | describe("'use client' directive", () => { 11 | test("throws and error when there is no directive and doesn't modify the program", async () => { 12 | const program = await getProgram("directive/no-directive.js"); 13 | 14 | expect(() => transform(program, OPTIONS)).toThrowError(); 15 | 16 | const code = generate(program); 17 | 18 | expect(code).toMatchSnapshot(); 19 | }); 20 | 21 | test("transforms when the directive is found at the top of the file or after other directives", async () => { 22 | const program = await getProgram("directive/valid.js"); 23 | 24 | transform(program, OPTIONS); 25 | 26 | const code = generate(program); 27 | 28 | expect(code).toMatchSnapshot(); 29 | }); 30 | 31 | test("throws and error when the directive is not at the top of the file and doesn't modify the program", async () => { 32 | const program = await getProgram("directive/invalid.js"); 33 | 34 | expect(() => transform(program, OPTIONS)).toThrowError(); 35 | 36 | const code = generate(program); 37 | 38 | expect(code).toMatchSnapshot(); 39 | }); 40 | }); 41 | 42 | const OPTIONS: TransformOptions = { 43 | generator: new Generator({ 44 | getRegisterArguments: ({ exportName }) => [builders.literal(exportName)], 45 | registerClientReferenceSource: "react-just", 46 | }), 47 | treeshakeImplementation: false, 48 | }; 49 | 50 | async function getProgram(filepath: string) { 51 | const code = await fs.readFile( 52 | path.resolve(import.meta.dirname, "fixtures", filepath), 53 | "utf-8", 54 | ); 55 | 56 | return parseAstAsync(code); 57 | } 58 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-client/transform/index.ts: -------------------------------------------------------------------------------- 1 | import { Program } from "estree"; 2 | import { traverse } from "estree-toolkit"; 3 | import { getUseClientDirective } from "../directive"; 4 | import transformExportDefaultDeclaration from "./export-default-declaration"; 5 | import transformExportNamedDeclaration from "./export-named-declaration"; 6 | import transformExportNamedFromSource from "./export-named-from-source"; 7 | import transformExportNamedSpecifiers from "./export-named-specifiers"; 8 | import Generator from "./generator"; 9 | import Module from "./module"; 10 | 11 | export default function transform(program: Program, options: TransformOptions) { 12 | const { generator, treeshakeImplementation } = options; 13 | 14 | const module = new Module(program); 15 | 16 | const useClientDirective = getUseClientDirective(program); 17 | 18 | if (!useClientDirective) 19 | throw new Error('Expected "use client" directive to exist'); 20 | 21 | module.remove(useClientDirective); 22 | 23 | module.unshift(generator.createRegisterFunctionImport()); 24 | 25 | traverse(program, { 26 | ExportAllDeclaration() { 27 | throw new Error( 28 | "export all (`export *`) declarations are not supported on client modules", 29 | ); 30 | }, 31 | ExportNamedDeclaration(path) { 32 | const node = path.node!; 33 | if (node.source) { 34 | transformExportNamedFromSource(node, module, generator); 35 | } else if (node.declaration) { 36 | transformExportNamedDeclaration(node, module, generator); 37 | } else { 38 | transformExportNamedSpecifiers(node, module, generator); 39 | } 40 | }, 41 | ExportDefaultDeclaration(path) { 42 | const node = path.node!; 43 | transformExportDefaultDeclaration(node, module, generator); 44 | }, 45 | }); 46 | 47 | if (treeshakeImplementation) 48 | program.body = generator.createTreeshakedBody(program.body); 49 | } 50 | 51 | export type TransformOptions = { 52 | generator: Generator; 53 | treeshakeImplementation: boolean; 54 | }; 55 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/directive.test.ts: -------------------------------------------------------------------------------- 1 | import transform, { TransformOptions } from "@/vite/use-server/transform"; 2 | import Generator from "@/vite/use-server/transform/generator"; 3 | import { generate } from "astring"; 4 | import { builders } from "estree-toolkit"; 5 | import fs from "node:fs/promises"; 6 | import path from "node:path"; 7 | import { parseAstAsync } from "vite"; 8 | import { describe, expect, test } from "vitest"; 9 | 10 | describe("'use server' module level directive", () => { 11 | test("throws an error when there is no directive and doesn't modify the program", async () => { 12 | const program = await getProgram("directive/no-directive.js"); 13 | 14 | expect(() => transform(program, OPTIONS)).toThrowError(); 15 | 16 | const code = generate(program); 17 | 18 | expect(code).toMatchSnapshot(); 19 | }); 20 | 21 | test("transforms when the directive is found at the top of the file or after other directives", async () => { 22 | const program = await getProgram("directive/valid-module-level.js"); 23 | 24 | transform(program, OPTIONS); 25 | 26 | const code = generate(program); 27 | 28 | expect(code).toMatchSnapshot(); 29 | }); 30 | 31 | test("throws an error when the directive is not at the top of the file and doesn't modify the program", async () => { 32 | const program = await getProgram("directive/invalid-module-level.js"); 33 | 34 | expect(() => transform(program, OPTIONS)).toThrowError(); 35 | 36 | const code = generate(program); 37 | 38 | expect(code).toMatchSnapshot(); 39 | }); 40 | }); 41 | 42 | const OPTIONS: TransformOptions = { 43 | generator: new Generator({ 44 | getRegisterArguments: ({ exportName }) => [builders.literal(exportName)], 45 | registerServerReferenceSource: "react-just", 46 | }), 47 | treeshakeImplementation: false, 48 | }; 49 | 50 | async function getProgram(filepath: string) { 51 | const code = await fs.readFile( 52 | path.resolve(import.meta.dirname, "fixtures", filepath), 53 | "utf-8", 54 | ); 55 | 56 | return parseAstAsync(code); 57 | } 58 | -------------------------------------------------------------------------------- /templates/node-js/src/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | 3 | import viteLogo from "./assets/vite.svg"; 4 | import ClientCounter from "./ClientCounter"; 5 | import { getCount } from "./db"; 6 | import ServerCounter from "./ServerCounter"; 7 | 8 | export default async function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | React Just is Awesome! 16 | 17 | 18 |
62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /templates/node-ts/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | 3 | import viteLogo from "./assets/vite.svg"; 4 | import ClientCounter from "./ClientCounter"; 5 | import { getCount } from "./db"; 6 | import ServerCounter from "./ServerCounter"; 7 | 8 | export default async function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | React Just is Awesome! 16 | 17 | 18 |
19 |

Hello, React Just!

20 | 21 | React Just 26 | 27 |
28 |
29 |

30 | This is a client counter. If you refresh the page, the count 31 | will reset. 32 |

33 | 34 |
35 |
36 |

37 | This is a server counter. If you refresh the page, the count 38 | will not reset. 39 |

40 | 41 |
42 |
43 |

44 | Click on the React Just logo or{" "} 45 | 46 | here 47 | {" "} 48 | to learn more 49 |

50 |
51 |

52 | Powered by{" "} 53 | 54 | Vite 55 | 56 |

57 | 58 | Vite 59 | 60 |
61 |
62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /templates/vercel-ts/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | 3 | import viteLogo from "./assets/vite.svg"; 4 | import ClientCounter from "./ClientCounter"; 5 | import { getCount } from "./db"; 6 | import ServerCounter from "./ServerCounter"; 7 | 8 | export default async function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | React Just is Awesome! 16 | 17 | 18 |
19 |

Hello, React Just!

20 | 21 | React Just 26 | 27 |
28 |
29 |

30 | This is a client counter. If you refresh the page, the count 31 | will reset. 32 |

33 | 34 |
35 |
36 |

37 | This is a server counter. If you refresh the page, the count 38 | will not reset. 39 |

40 | 41 |
42 |
43 |

44 | Click on the React Just logo or{" "} 45 | 46 | here 47 | {" "} 48 | to learn more 49 |

50 |
51 |

52 | Powered by{" "} 53 | 54 | Vite 55 | 56 |

57 | 58 | Vite 59 | 60 |
61 |
62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /docs/reference/router/link.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2, 3] 3 | --- 4 | 5 | # Link 6 | 7 | `Link` is a component that provides declarative navigation between routes with single-page application behavior. 8 | 9 | ```tsx 10 | import { Link } from "@react-just/router"; 11 | 12 | function Navigation() { 13 | return ( 14 | 19 | ); 20 | } 21 | ``` 22 | 23 | ## Props 24 | 25 | ```tsx 26 | interface LinkProps extends React.DOMAttributes { 27 | href: string; 28 | replace?: boolean; 29 | } 30 | ``` 31 | 32 | - `href`: The URL path to navigate to. Can be a relative or absolute URL. 33 | - `replace` (optional): Replace the current entry in the history stack instead of pushing a new one. Defaults to `false`. 34 | - All standard anchor element attributes and event handlers are supported. 35 | 36 | ## Behavior 37 | 38 | The `Link` component renders as an anchor tag but intercepts clicks to provide client-side navigation. It automatically handles: 39 | 40 | - **External links**: Links to different origins are handled normally (full page navigation). 41 | - **Modified clicks**: Clicks with modifier keys (Ctrl, Cmd, Shift, Alt) open in new tabs/windows. 42 | - **Special targets**: Links with `target` attributes other than `_self` are handled normally. 43 | - **Downloads**: Links with `download` attributes are handled normally. 44 | 45 | ## Examples 46 | 47 | ### Basic Navigation 48 | 49 | ```tsx 50 | import { Link } from "@react-just/router"; 51 | 52 | function Navigation() { 53 | // Current URL -> /shop/shirts/item3 54 | return ( 55 | 61 | ); 62 | } 63 | ``` 64 | 65 | ### Navigation with Replace 66 | 67 | ```tsx 68 | import { Link } from "@react-just/router"; 69 | 70 | function TabsHeader() { 71 | return ( 72 |
73 | 74 | Overview 75 | 76 | 77 | Details 78 | 79 | 80 | Settings 81 | 82 |
83 | ); 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-client/transform/export-named-from-source.ts: -------------------------------------------------------------------------------- 1 | import { ExportNamedDeclaration, ImportSpecifier } from "estree"; 2 | import { builders } from "estree-toolkit"; 3 | import Generator from "./generator"; 4 | import Module from "./module"; 5 | 6 | /** 7 | * Transforms exports in the form of: 8 | * ```ts 9 | * export { a, b as c, b as d, default, default as e } from "pkg"; 10 | * ``` 11 | * 12 | * Into: 13 | * ```ts 14 | * import { 15 | * a as $$a$$, 16 | * b as $$c$$, 17 | * b as $$d$$, 18 | * default as $$default$$, 19 | * default as $$e$$ 20 | * } from "pkg"; 21 | * const $$Ref$$a = $$registerClientReference$$(...); 22 | * const $$Ref$$c = $$registerClientReference$$(...); 23 | * const $$Ref$$d = $$registerClientReference$$(...); 24 | * const $$Ref$$default = $$registerClientReference$$(...); 25 | * const $$Ref$$e = $$registerClientReference$$(...); 26 | * export { 27 | * $$Ref$$a as a, 28 | * $$Ref$$c as c, 29 | * $$Ref$$d as d, 30 | * $$Ref$$default as default, 31 | * $$Ref$$e as e 32 | * }; 33 | * ``` 34 | */ 35 | export default function transformExportNamedFromSource( 36 | node: ExportNamedDeclaration, 37 | module: Module, 38 | generator: Generator, 39 | ) { 40 | const source = node.source!; 41 | 42 | const importSpecifiers: ImportSpecifier[] = []; 43 | 44 | for (const specifier of node.specifiers) { 45 | if ( 46 | specifier.local.type !== "Identifier" || 47 | specifier.exported.type !== "Identifier" 48 | ) 49 | /* c8 ignore next */ 50 | continue; 51 | 52 | const importIdentifier = specifier.local.name; 53 | const exportIdentifier = specifier.exported.name; 54 | // It's allowed to export an identifier from a module with the same 55 | // name as another identifier within the same module. Add the $$ 56 | // prefix and suffix to avoid collisions. 57 | const localIdentifier = "$$" + exportIdentifier + "$$"; 58 | 59 | importSpecifiers.push( 60 | builders.importSpecifier( 61 | builders.identifier(importIdentifier), 62 | builders.identifier(localIdentifier), 63 | ), 64 | ); 65 | 66 | module.append( 67 | ...generator.createRegisterAndExportReference( 68 | exportIdentifier, 69 | localIdentifier, 70 | ), 71 | ); 72 | } 73 | 74 | module.unshift(builders.importDeclaration(importSpecifiers, source)); 75 | 76 | module.remove(node); 77 | } 78 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-server/transform/export-named-from-source.ts: -------------------------------------------------------------------------------- 1 | import { ExportNamedDeclaration, ImportSpecifier } from "estree"; 2 | import { builders } from "estree-toolkit"; 3 | import Generator from "./generator"; 4 | import Module from "./module"; 5 | 6 | /** 7 | * Transforms exports in the form of: 8 | * ```ts 9 | * export { a, b as c, b as d, default, default as e } from "pkg"; 10 | * ``` 11 | * 12 | * Into: 13 | * ```ts 14 | * import { 15 | * a as $$a$$, 16 | * b as $$c$$, 17 | * b as $$d$$, 18 | * default as $$default$$, 19 | * default as $$e$$ 20 | * } from "pkg"; 21 | * const $$Ref$$a = $$registerServerReference$$(...); 22 | * const $$Ref$$c = $$registerServerReference$$(...); 23 | * const $$Ref$$d = $$registerServerReference$$(...); 24 | * const $$Ref$$default = $$registerServerReference$$(...); 25 | * const $$Ref$$e = $$registerServerReference$$(...); 26 | * export { 27 | * $$Ref$$a as a, 28 | * $$Ref$$c as c, 29 | * $$Ref$$d as d, 30 | * $$Ref$$default as default, 31 | * $$Ref$$e as e 32 | * }; 33 | * ``` 34 | */ 35 | export default function transformExportNamedFromSource( 36 | node: ExportNamedDeclaration, 37 | module: Module, 38 | generator: Generator, 39 | ) { 40 | const source = node.source!; 41 | 42 | const importSpecifiers: ImportSpecifier[] = []; 43 | 44 | for (const specifier of node.specifiers) { 45 | if ( 46 | specifier.local.type !== "Identifier" || 47 | specifier.exported.type !== "Identifier" 48 | ) 49 | /* c8 ignore next */ 50 | continue; 51 | 52 | const importIdentifier = specifier.local.name; 53 | const exportIdentifier = specifier.exported.name; 54 | // It's allowed to export an identifier from a module with the same 55 | // name as another identifier within the same module. Add the $$ 56 | // prefix and suffix to avoid collisions. 57 | const localIdentifier = "$$" + exportIdentifier + "$$"; 58 | 59 | importSpecifiers.push( 60 | builders.importSpecifier( 61 | builders.identifier(importIdentifier), 62 | builders.identifier(localIdentifier), 63 | ), 64 | ); 65 | 66 | module.append( 67 | ...generator.createRegisterAndExportReference( 68 | exportIdentifier, 69 | localIdentifier, 70 | ), 71 | ); 72 | } 73 | 74 | module.unshift(builders.importDeclaration(importSpecifiers, source)); 75 | 76 | module.remove(node); 77 | } 78 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-client/transform/export-default-declaration.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassDeclaration, 3 | ExportDefaultDeclaration, 4 | FunctionDeclaration, 5 | } from "estree"; 6 | import { builders } from "estree-toolkit"; 7 | import Generator from "./generator"; 8 | import Module from "./module"; 9 | 10 | /** 11 | * Transforms exports in the form of: 12 | * ```ts 13 | * let a; 14 | * export default a; 15 | * 16 | * export default function b() {} 17 | * 18 | * export default []; 19 | * ``` 20 | * 21 | * Into: 22 | * ```ts 23 | * let a; 24 | * const $$Ref$$default = $$registerClientReference$$(...); 25 | * export { $$Ref$$default as default }; 26 | * 27 | * function b() {} 28 | * const $$Ref$$default = $$registerClientReference$$(...); 29 | * export { $$Ref$$default as default }; 30 | * 31 | * const $$default$$ = []; 32 | * const $$Ref$$default = $$registerClientReference$$(...); 33 | * export { $$Ref$$default as default }; 34 | * ``` 35 | */ 36 | export default function transformExportDefaultDeclaration( 37 | node: ExportDefaultDeclaration, 38 | module: Module, 39 | generator: Generator, 40 | ) { 41 | let implementationIdentifier: string; 42 | 43 | if (node.declaration.type === "Identifier") { 44 | // let a; 45 | // export default a; 46 | implementationIdentifier = node.declaration.name; 47 | 48 | module.remove(node); 49 | } else if ( 50 | node.declaration.type === "FunctionDeclaration" || 51 | node.declaration.type === "ClassDeclaration" 52 | ) { 53 | // export default function b() {} 54 | if (!node.declaration.id) 55 | node.declaration.id = builders.identifier("$$default$$"); 56 | 57 | implementationIdentifier = node.declaration.id.name; 58 | 59 | module.replace( 60 | node, 61 | node.declaration as FunctionDeclaration | ClassDeclaration, // identifier is now defined 62 | ); 63 | } else { 64 | // export default []; 65 | implementationIdentifier = "$$default$$"; 66 | 67 | const variableDeclaration = builders.variableDeclaration("const", [ 68 | builders.variableDeclarator( 69 | builders.identifier(implementationIdentifier), 70 | node.declaration, 71 | ), 72 | ]); 73 | 74 | module.replace(node, variableDeclaration); 75 | } 76 | 77 | module.append( 78 | ...generator.createRegisterAndExportReference( 79 | "default", 80 | implementationIdentifier, 81 | ), 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-server/transform/export-default-declaration.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassDeclaration, 3 | ExportDefaultDeclaration, 4 | FunctionDeclaration, 5 | } from "estree"; 6 | import { builders } from "estree-toolkit"; 7 | import Generator from "./generator"; 8 | import Module from "./module"; 9 | 10 | /** 11 | * Transforms exports in the form of: 12 | * ```ts 13 | * let a; 14 | * export default a; 15 | * 16 | * export default function b() {} 17 | * 18 | * export default () => {} 19 | * ``` 20 | * 21 | * Into: 22 | * ```ts 23 | * let a; 24 | * const $$Ref$$default = $$registerServerReference$$(...); 25 | * export { $$Ref$$default as default }; 26 | * 27 | * function b() {} 28 | * const $$Ref$$default = $$registerServerReference$$(...); 29 | * export { $$Ref$$default as default }; 30 | * 31 | * const $$default$$ = () => {}; 32 | * const $$Ref$$default = $$registerServerReference$$(...); 33 | * export { $$Ref$$default as default }; 34 | * ``` 35 | */ 36 | export default function transformExportDefaultDeclaration( 37 | node: ExportDefaultDeclaration, 38 | module: Module, 39 | generator: Generator, 40 | ) { 41 | let implementationIdentifier: string; 42 | 43 | if (node.declaration.type === "Identifier") { 44 | // let a; 45 | // export default a; 46 | implementationIdentifier = node.declaration.name; 47 | 48 | module.remove(node); 49 | } else if ( 50 | node.declaration.type === "FunctionDeclaration" || 51 | node.declaration.type === "ClassDeclaration" 52 | ) { 53 | // export default function b() {} 54 | if (!node.declaration.id) 55 | node.declaration.id = builders.identifier("$$default$$"); 56 | 57 | implementationIdentifier = node.declaration.id.name; 58 | 59 | module.replace( 60 | node, 61 | node.declaration as FunctionDeclaration | ClassDeclaration, // identifier is now defined 62 | ); 63 | } else { 64 | // export default []; 65 | implementationIdentifier = "$$default$$"; 66 | 67 | const variableDeclaration = builders.variableDeclaration("const", [ 68 | builders.variableDeclarator( 69 | builders.identifier(implementationIdentifier), 70 | node.declaration, 71 | ), 72 | ]); 73 | 74 | module.replace(node, variableDeclaration); 75 | } 76 | 77 | module.append( 78 | ...generator.createRegisterAndExportReference( 79 | "default", 80 | implementationIdentifier, 81 | ), 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /packages/node/src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { HandleFunction } from "@/types/handle"; 4 | import { program } from "commander"; 5 | import mime from "mime/lite"; 6 | import fs from "node:fs/promises"; 7 | import http from "node:http"; 8 | import path from "node:path"; 9 | import { 10 | DEFAULT_BUILD_PATH, 11 | SERVER_DIR, 12 | SERVER_ENTRY_FILENAME, 13 | STATIC_DIR, 14 | } from "./constants"; 15 | 16 | program 17 | .option("-p, --port ", "Port to listen on", "3000") 18 | .option("--no-static", "Disable static files serving") 19 | .argument("[build-path]", "Build directory", DEFAULT_BUILD_PATH); 20 | 21 | program.parse(process.argv); 22 | 23 | const [buildPath = DEFAULT_BUILD_PATH] = program.args; 24 | const { port, static: serverStatic = true } = program.opts(); 25 | 26 | try { 27 | await assertIsDirectory(path.resolve(buildPath)); 28 | 29 | const { default: handle } = await import( 30 | path.resolve(buildPath, SERVER_DIR, SERVER_ENTRY_FILENAME) 31 | ); 32 | 33 | const server = http.createServer( 34 | serverStatic ? await handleWithStaticFiles(handle) : handle, 35 | ); 36 | 37 | server.listen(port, () => { 38 | console.log(`React Just server running on port ${port}`); 39 | }); 40 | } catch (error) { 41 | if (error instanceof Error) console.error(error.message); 42 | else console.error(error); 43 | 44 | process.exit(1); 45 | } 46 | 47 | async function handleWithStaticFiles( 48 | handle: HandleFunction, 49 | ): Promise { 50 | const staticDir = path.resolve(buildPath, STATIC_DIR); 51 | 52 | await assertIsDirectory(staticDir); 53 | 54 | return async (req, res) => { 55 | const staticFile = await getStaticFile(staticDir, req.url ?? ""); 56 | 57 | if (staticFile) { 58 | res.statusCode = 200; 59 | res.setHeader("content-type", staticFile.mimeType); 60 | res.end(staticFile.buffer); 61 | return; 62 | } 63 | 64 | handle(req, res); 65 | }; 66 | } 67 | 68 | async function assertIsDirectory(dir: string) { 69 | try { 70 | await fs.access(dir, fs.constants.F_OK); 71 | } catch { 72 | throw new Error(`Expected ${dir} directory to exist`); 73 | } 74 | 75 | const stat = await fs.stat(dir); 76 | if (!stat.isDirectory()) throw new Error(`Expected ${dir} to be a directory`); 77 | } 78 | 79 | async function getStaticFile(staticDir: string, url: string) { 80 | try { 81 | const relativePath = url.replace(/^\/|\/$/g, ""); 82 | 83 | const staticFilePath = path.resolve(staticDir, relativePath); 84 | 85 | // Prevent directory traversal attacks 86 | if (!staticFilePath.startsWith(staticDir)) return null; 87 | 88 | return { 89 | buffer: await fs.readFile(staticFilePath), 90 | mimeType: mime.getType(staticFilePath) ?? "application/octet-stream", 91 | }; 92 | } catch { 93 | return null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /docs/reference/core/server.md: -------------------------------------------------------------------------------- 1 | # Server Utilities (`react-just/server`) 2 | 3 | :::warning Server Only 4 | These APIs can only be used in **Server Components**, **Server Functions**, or their transitive dependencies. They cannot be used in **Client Components** or in code that runs in the browser. 5 | ::: 6 | 7 | ## `request` 8 | 9 | Access information about the incoming HTTP request. 10 | 11 | ```tsx 12 | import { request } from "react-just/server"; 13 | 14 | async function RequestDetails() { 15 | const { url, method, headers } = request(); 16 | 17 | const parsed = new URL(url); 18 | const device = await getDevice(headers.get("user-agent")); 19 | 20 | return ( 21 |
22 |

Method: {method}

23 |

Pathname: {parsed.pathname}

24 |

Device: {device}

25 |
26 | ); 27 | } 28 | ``` 29 | 30 | ```ts 31 | "use server"; 32 | 33 | export async function addComment() { 34 | const { headers } = request(); 35 | 36 | const device = await getDevice(headers.get("user-agent")); 37 | 38 | // ... 39 | } 40 | ``` 41 | 42 | ### Parameters 43 | 44 | ```ts 45 | const req = request(); 46 | ``` 47 | 48 | `request` takes no parameters. 49 | 50 | ### Returns 51 | 52 | ```ts 53 | function request(): JustRequest; 54 | 55 | interface JustRequest { 56 | readonly headers: Headers; 57 | readonly method: string; 58 | readonly url: string; 59 | } 60 | ``` 61 | 62 | `request` returns an object with the following properties: 63 | 64 | - `headers`: A standard [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) representing the request headers. 65 | - `method`: The HTTP method of the request (e.g., `GET`, `POST`). 66 | - `url`: The absolute URL of the request. You can create a `URL` instance from it. 67 | 68 | ## `response` 69 | 70 | Access and modify the outgoing HTTP response of a Server Function. 71 | 72 | :::warning Server Functions Only 73 | Changes to the response made during rendering will not take effect. 74 | ::: 75 | 76 | ```ts 77 | "use server"; 78 | 79 | import { response } from "react-just/server"; 80 | 81 | export async function logout() { 82 | const res = response(); 83 | 84 | res.headers.append("Set-Cookie", "sessionId=; Max-Age=0; Path=/; HttpOnly"); 85 | } 86 | ``` 87 | 88 | ### Parameters 89 | 90 | ```ts 91 | const res = response(); 92 | ``` 93 | 94 | `response` takes no parameters. 95 | 96 | ### Returns 97 | 98 | ```ts 99 | declare function response(): JustResponse; 100 | 101 | interface JustResponse { 102 | readonly headers: Headers; 103 | } 104 | ``` 105 | 106 | `response` returns an object containing the following properties: 107 | 108 | - `headers`: A standard [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object representing the response headers. Use it to set headers like `Set-Cookie`. 109 | -------------------------------------------------------------------------------- /packages/router/src/components/NavigateProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Navigate } from "@/types"; 4 | import { ReactNode, useCallback, useEffect } from "react"; 5 | import { createFromRscFetch, render, RSC_MIME_TYPE } from "react-just/client"; 6 | import NavigateContext from "../context/navigate"; 7 | import { removeTrailingSlashes } from "../utils"; 8 | 9 | interface NavigateProviderProps { 10 | children: ReactNode; 11 | trailingSlashes: "remove"; 12 | } 13 | 14 | export default function NavigateProvider({ 15 | children, 16 | trailingSlashes, 17 | }: NavigateProviderProps) { 18 | useEffect(() => { 19 | if (trailingSlashes === "remove") 20 | window.history.replaceState( 21 | null, 22 | "", 23 | removeTrailingSlashes(new URL(window.location.href)), 24 | ); 25 | }, [trailingSlashes]); 26 | 27 | useEffect(() => { 28 | const originalPushState = window.history.pushState; 29 | const originalReplaceState = window.history.replaceState; 30 | 31 | window.addEventListener("popstate", onNavigation); 32 | 33 | window.history.pushState = function (...args) { 34 | originalPushState.apply(this, args); 35 | onNavigation(); 36 | }; 37 | 38 | window.history.replaceState = function (...args) { 39 | originalReplaceState.apply(this, args); 40 | onNavigation(); 41 | }; 42 | 43 | return () => { 44 | window.removeEventListener("popstate", onNavigation); 45 | window.history.pushState = originalPushState; 46 | window.history.replaceState = originalReplaceState; 47 | }; 48 | }, []); 49 | 50 | const navigate = useCallback( 51 | (hrefOrDelta, options) => { 52 | if (typeof hrefOrDelta === "number") { 53 | window.history.go(hrefOrDelta); 54 | return; 55 | } 56 | 57 | let url = new URL(hrefOrDelta, window.location.href); 58 | 59 | if (trailingSlashes === "remove") url = removeTrailingSlashes(url); 60 | 61 | if (options?.replace) { 62 | window.history.replaceState(null, "", url); 63 | } else { 64 | window.history.pushState(null, "", url); 65 | } 66 | }, 67 | [trailingSlashes], 68 | ); 69 | 70 | return {children}; 71 | } 72 | 73 | let currentNavId = 0; 74 | 75 | function onNavigation() { 76 | const navId = ++currentNavId; 77 | 78 | // Don't use exactly the same URL as the one we're trying to load to avoid 79 | // sharing cache between the RSC and the HTML. 80 | const url = new URL(window.location.href); 81 | url.searchParams.set("__rsc__", "1"); 82 | 83 | createFromRscFetch<{ tree: ReactNode }>( 84 | fetch(url, { headers: { accept: RSC_MIME_TYPE } }), 85 | ).then(({ tree }) => { 86 | // Avoid race conditions between multiple navigation events. Render only the latest one. 87 | if (currentNavId === navId) render(tree); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /packages/react-just/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-just", 3 | "version": "0.4.1", 4 | "type": "module", 5 | "description": "React Server Components without a framework", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/almadoro/react-just.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/almadoro/react-just/issues" 13 | }, 14 | "keywords": [ 15 | "vite", 16 | "vite plugin", 17 | "vite-plugin", 18 | "react", 19 | "reactjs", 20 | "react server components", 21 | "rsc", 22 | "server components" 23 | ], 24 | "files": [ 25 | "dist", 26 | "types" 27 | ], 28 | "exports": { 29 | "./client": { 30 | "types": "./types/client.d.ts", 31 | "import": "./dist/client.mjs", 32 | "require": "./dist/client.cjs" 33 | }, 34 | "./fizz.node": { 35 | "types": "./types/fizz.node.d.ts", 36 | "import": "./dist/fizz.node.mjs", 37 | "require": "./dist/fizz.node.cjs" 38 | }, 39 | "./flight.node": { 40 | "types": "./types/flight.node.d.ts", 41 | "import": "./dist/flight.node.mjs", 42 | "require": "./dist/flight.node.cjs" 43 | }, 44 | "./handle.node": { 45 | "types": "./types/handle.node.d.ts", 46 | "import": "./dist/handle.node.mjs", 47 | "require": "./dist/handle.node.cjs" 48 | }, 49 | "./server": { 50 | "types": "./types/server.d.ts", 51 | "import": { 52 | "react-server": "./dist/server.node.mjs", 53 | "default": "./dist/server.error.mjs" 54 | }, 55 | "require": { 56 | "react-server": "./dist/server.node.cjs", 57 | "default": "./dist/server.error.cjs" 58 | } 59 | }, 60 | "./vite": { 61 | "types": "./types/vite.d.ts", 62 | "import": "./dist/vite.mjs", 63 | "require": "./dist/vite.cjs" 64 | } 65 | }, 66 | "scripts": { 67 | "build": "rm -rf dist && pnpm check && rollup -c", 68 | "check": "tsc --noEmit -p tsconfig.build.json", 69 | "test": "vitest", 70 | "test:coverage": "vitest --coverage", 71 | "test:open": "xdg-open coverage/index.html", 72 | "test:update": "vitest --update" 73 | }, 74 | "dependencies": { 75 | "@vitejs/plugin-react": "^4.4.1", 76 | "astring": "^1.9.0", 77 | "busboy": "^1.6.0", 78 | "estree-toolkit": "^1.7.13" 79 | }, 80 | "devDependencies": { 81 | "@rollup/plugin-node-resolve": "^16.0.1", 82 | "@rollup/plugin-typescript": "^12.1.2", 83 | "@types/busboy": "^1.5.4", 84 | "@types/estree": "^1.0.7", 85 | "@types/react": "^19.1.4", 86 | "@types/react-dom": "^19.1.5", 87 | "@vitest/coverage-v8": "3.2.4", 88 | "esbuild": "^0.25.5", 89 | "rollup": "^4.43.0", 90 | "typescript": "^5.9.2", 91 | "vite": "^7.0.6", 92 | "vitest": "^3.2.4" 93 | }, 94 | "peerDependencies": { 95 | "react": "~19.1", 96 | "react-dom": "~19.1", 97 | "react-server-dom-webpack": "~19.1", 98 | "vite": ">=7" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /docs/guide/deploy/node.md: -------------------------------------------------------------------------------- 1 | # Node.js Deployment 2 | 3 | Deploy your React Just app using a Node.js environment. 4 | 5 | ## Installation 6 | 7 | Install the Node.js adapter package in your project: 8 | 9 | ::: code-group 10 | 11 | ```bash [npm] 12 | $ npm install @react-just/node 13 | ``` 14 | 15 | ```bash [pnpm] 16 | $ pnpm add @react-just/node 17 | ``` 18 | 19 | ```bash [bun] 20 | $ bun add @react-just/node 21 | ``` 22 | 23 | ::: 24 | 25 | Add the plugin in the Vite config file: 26 | 27 | ```ts [vite.config.ts] {1,6} 28 | import node from "@react-just/node"; 29 | import react from "react-just/vite"; 30 | import { defineConfig } from "vite"; 31 | 32 | export default defineConfig({ 33 | plugins: [react(), node()], 34 | }); 35 | ``` 36 | 37 | ## Building the App 38 | 39 | Build the app with the `vite build` command. For convenience, add the following script to your `package.json`: 40 | 41 | ```json [package.json] {3} 42 | { 43 | "scripts": { 44 | "build": "vite build" 45 | } 46 | } 47 | ``` 48 | 49 | Then build the app with: 50 | 51 | ::: code-group 52 | 53 | ```bash [npm] 54 | $ npm run build 55 | ``` 56 | 57 | ```bash [pnpm] 58 | $ pnpm build 59 | ``` 60 | 61 | ```bash [Bun] 62 | $ bun run build 63 | ``` 64 | 65 | ::: 66 | 67 | By default, the build will be placed in the `dist` directory. You can change it with the `build.outDir` Vite config option: 68 | 69 | ```ts [vite.config.ts] {7} 70 | import node from "@react-just/node"; 71 | import react from "react-just/vite"; 72 | import { defineConfig } from "vite"; 73 | 74 | export default defineConfig({ 75 | plugins: [react(), node()], 76 | build: { outDir: "lib" }, 77 | }); 78 | ``` 79 | 80 | ### The `static` Folder 81 | 82 | Static assets are emitted to the `static` folder in the output directory. These can be served directly (e.g., from object storage like S3 behind a CDN). 83 | 84 | ## Start the Server 85 | 86 | Start a server using the `react-just-node` command. 87 | 88 | ::: code-group 89 | 90 | ```bash [npm] 91 | $ npx react-just-node 92 | ``` 93 | 94 | ```bash [pnpm] 95 | $ pnpm react-just-node 96 | ``` 97 | 98 | ```bash [bun] 99 | $ bun react-just-node 100 | ``` 101 | 102 | ::: 103 | 104 | By default, this will: 105 | 106 | - Serve the build in the `dist` directory. 107 | - Statically serve the files in the `dist/static` directory. 108 | - Listen on port `3000`, making your app available at [`http://localhost:3000`](http://localhost:3000). 109 | 110 | ### Add a Script 111 | 112 | For convenience, add a `start` script to your `package.json`: 113 | 114 | ```json [package.json] {3} 115 | { 116 | "scripts": { 117 | "start": "react-just-node" 118 | } 119 | } 120 | ``` 121 | 122 | Then start your app with: 123 | 124 | ::: code-group 125 | 126 | ```bash [npm] 127 | $ npm run start 128 | ``` 129 | 130 | ```bash [pnpm] 131 | $ pnpm start 132 | ``` 133 | 134 | ```bash [bun] 135 | $ bun start 136 | ``` 137 | 138 | ::: 139 | 140 | For details about the adapter and CLI options, see the [Node.js adapter reference](/reference/platforms/node). 141 | -------------------------------------------------------------------------------- /docs/guide/routing.md: -------------------------------------------------------------------------------- 1 | # Routing 2 | 3 | By design, React Just lets you use your preferred routing solution, skip routing completely, or even build your own. 4 | 5 | ## React Just Router 6 | 7 | [`@react-just/router`](/reference/router) is a complementary package that includes the routing features that the most common use cases require: static routes, path and wildcard parameters, nested routes and shared layouts. 8 | 9 | It has a familiar and simple API. It uses JSX to define routes, provides utility hooks to read route data or programmatically navigate, and allows both Client and Server Components to be used as route components. 10 | 11 | ### Installation 12 | 13 | ::: code-group 14 | 15 | ```bash [npm] 16 | $ npm install @react-just/router 17 | ``` 18 | 19 | ```bash [pnpm] 20 | $ pnpm add @react-just/router 21 | ``` 22 | 23 | ```bash [Bun] 24 | $ bun add @react-just/router 25 | ``` 26 | 27 | ::: 28 | 29 | ### Example 30 | 31 | ```tsx [src/index.tsx] 32 | import { Route, Router } from "@react-just/router"; 33 | import { request } from "react-just/server"; 34 | import Home from "./Home"; 35 | import OrderDetail from "./OrderDetail"; 36 | import OrdersLayout from "./OrdersLayout"; 37 | 38 | export default async function App() { 39 | const { url } = request(); 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | ``` 54 | 55 | ```tsx [src/OrdersLayout.tsx] 56 | import type { RouteComponentProps } from "@react-just/router"; 57 | 58 | export default function OrdersLayout({ children }: RouteComponentProps) { 59 | return ( 60 |
61 |

Orders

62 | Loading...

}>{children}
63 |
64 | ); 65 | } 66 | ``` 67 | 68 | ```tsx [src/OrderDetail.tsx] 69 | import type { RouteComponentProps } from "@react-just/router"; 70 | import { getOrderDetails } from "./queries"; 71 | 72 | export default async function OrderDetail({ 73 | params, 74 | }: RouteComponentProps<{ orderId: string }>) { 75 | const details = await getOrderDetails(params.orderId); 76 | return ( 77 |
78 |

Order ID: {params.orderId}

79 |
{details}
80 |
81 | ); 82 | } 83 | ``` 84 | 85 | Visit the [Router reference](/reference/router) for more usage details. 86 | 87 | ## Build Your Own 88 | 89 | React Just exposes a client API that lets you load and render a page in RSC format. You can use this to implement your own navigation logic. 90 | 91 | ```tsx 92 | import { createFromRscFetch, render, RSC_MIME_TYPE } from "react-just/client"; 93 | 94 | function onNavigation() { 95 | createFromRscFetch( 96 | fetch(window.location.href, { headers: { accept: RSC_MIME_TYPE } }), 97 | ).then(({ tree }) => render(tree)); 98 | } 99 | ``` 100 | 101 | Visit the [Low Level APIs reference](/reference/core) for more information. 102 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-server/transform/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArrowFunctionExpression, 3 | FunctionDeclaration, 4 | FunctionExpression, 5 | Program, 6 | } from "estree"; 7 | import { NodePath, traverse } from "estree-toolkit"; 8 | import { 9 | getIsUseServerDirective, 10 | getUseServerModuleDirective, 11 | } from "../directive"; 12 | import transformExportDefaultDeclaration from "./export-default-declaration"; 13 | import transformExportNamedDeclaration from "./export-named-declaration"; 14 | import transformExportNamedFromSource from "./export-named-from-source"; 15 | import transformExportNamedSpecifiers from "./export-named-specifiers"; 16 | import Generator from "./generator"; 17 | import Module from "./module"; 18 | 19 | export default function transform(program: Program, options: TransformOptions) { 20 | const { generator, treeshakeImplementation } = options; 21 | 22 | const module = new Module(program); 23 | 24 | // The directive "use server" directive can appear at the top 25 | // of a module. 26 | const useServerModuleDirective = getUseServerModuleDirective(program); 27 | 28 | if (useServerModuleDirective) module.remove(useServerModuleDirective); 29 | 30 | traverse(program, { 31 | ExportAllDeclaration() { 32 | if (!useServerModuleDirective) return; 33 | 34 | throw new Error( 35 | 'export all (`export *`) declarations are not supported on "use server" modules', 36 | ); 37 | }, 38 | ExportNamedDeclaration(path) { 39 | if (!useServerModuleDirective) return; 40 | 41 | const node = path.node!; 42 | if (node.source) { 43 | transformExportNamedFromSource(node, module, generator); 44 | } else if (node.declaration) { 45 | transformExportNamedDeclaration(node, module, generator); 46 | } else { 47 | transformExportNamedSpecifiers(node, module, generator); 48 | } 49 | }, 50 | ExportDefaultDeclaration(path) { 51 | if (!useServerModuleDirective) return; 52 | 53 | const node = path.node!; 54 | transformExportDefaultDeclaration(node, module, generator); 55 | }, 56 | // The directive "use server" directive can appear at the top of a function. 57 | ArrowFunctionExpression: FunctionLike, 58 | FunctionDeclaration: FunctionLike, 59 | FunctionExpression: FunctionLike, 60 | }); 61 | 62 | if (!useServerModuleDirective) 63 | throw new Error('Expected "use server" directive to exist'); 64 | 65 | module.unshift(generator.createRegisterFunctionImport()); 66 | 67 | if (treeshakeImplementation) 68 | program.body = generator.createTreeshakedBody(program.body); 69 | } 70 | 71 | export type TransformOptions = { 72 | generator: Generator; 73 | treeshakeImplementation: boolean; 74 | }; 75 | 76 | function FunctionLike( 77 | path: NodePath< 78 | FunctionDeclaration | FunctionExpression | ArrowFunctionExpression 79 | >, 80 | ) { 81 | const node = path.node!; 82 | 83 | if (node.body.type !== "BlockStatement") return; 84 | 85 | const firstBlockNode = node.body.body[0]; 86 | 87 | const isUseServerDirective = 88 | firstBlockNode && getIsUseServerDirective(firstBlockNode); 89 | 90 | if (!isUseServerDirective) return; 91 | 92 | const async = node.async; 93 | 94 | if (!async) throw new Error("server functions must be async"); 95 | 96 | throw new Error("use server directive inside functions not supported yet"); 97 | } 98 | -------------------------------------------------------------------------------- /docs/reference/router/use-navigate.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2, 3] 3 | --- 4 | 5 | # useNavigate 6 | 7 | `useNavigate` is a hook that returns a function to programmatically change routes. 8 | 9 | ```tsx 10 | "use client"; 11 | 12 | import { useNavigate } from "@react-just/router"; 13 | 14 | function SomeComponent() { 15 | const navigate = useNavigate(); 16 | 17 | return ; 18 | } 19 | ``` 20 | 21 | ## Parameters 22 | 23 | ```tsx 24 | const navigate = useNavigate(); 25 | ``` 26 | 27 | `useNavigate` takes no parameters. 28 | 29 | ## Returns 30 | 31 | `useNavigate` returns a navigate function with the following signatures: 32 | 33 | ```tsx 34 | function navigate(href: string, options?: NavigateOptions): void; 35 | function navigate(delta: number): void; 36 | 37 | interface NavigateOptions { 38 | replace?: boolean; 39 | } 40 | ``` 41 | 42 | The hook must be used within a [Router](/reference/router/router) component. Otherwise, it throws an error. 43 | 44 | ### Parameters 45 | 46 | **Navigate to URL** 47 | 48 | - `href`: The URL path to navigate to. Can be a relative URL. 49 | - `options`: Navigation options 50 | - `replace`: Replace the current entry in the history stack instead of pushing a new one 51 | 52 | **Navigate by delta** 53 | 54 | - `delta`: A positive or negative number of entries to move in the history stack (negative moves back, positive moves forward). 55 | 56 | ## Examples 57 | 58 | ### Navigate to Another Route 59 | 60 | ```tsx 61 | "use client"; 62 | 63 | import { useNavigate } from "@react-just/router"; 64 | 65 | function Home() { 66 | const navigate = useNavigate(); 67 | 68 | const goToAbout = () => navigate("/about"); 69 | const goToContact = () => navigate("/contact"); 70 | 71 | return ( 72 |
73 |

Home Page

74 | 75 | 76 |
77 | ); 78 | } 79 | ``` 80 | 81 | ### Navigate Back and Forward in History 82 | 83 | ```tsx 84 | "use client"; 85 | 86 | import { useNavigate } from "@react-just/router"; 87 | 88 | function Navigation() { 89 | const navigate = useNavigate(); 90 | 91 | return ( 92 |
93 | 94 | 95 | 96 |
97 | ); 98 | } 99 | ``` 100 | 101 | ### Navigate with Replace Option 102 | 103 | ```tsx 104 | "use client"; 105 | 106 | import { useNavigate, useSearchParams } from "@react-just/router"; 107 | 108 | function Search() { 109 | const navigate = useNavigate(); 110 | const searchParams = useSearchParams(); 111 | const query = searchParams.get("q"); 112 | 113 | const handleSearch = (e: React.FormEvent) => { 114 | e.preventDefault(); 115 | const q = (e.target as HTMLFormElement).query.value.trim(); 116 | const search = new URLSearchParams({ q }); 117 | if (q) navigate("?" + search.toString(), { replace: true }); 118 | }; 119 | 120 | return ( 121 |
122 |

Showing results for: {query}

123 | 124 | 125 |
126 | ); 127 | } 128 | ``` 129 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-server/environments.ts: -------------------------------------------------------------------------------- 1 | import { builders } from "estree-toolkit"; 2 | import crypto from "node:crypto"; 3 | import { ENVIRONMENTS } from "../environments"; 4 | import { TransformOptions } from "./transform"; 5 | import Generator from "./transform/generator"; 6 | 7 | export function getTransformOptions(options: { 8 | environment: string; 9 | hash: boolean; 10 | relativePath: string; 11 | }): TransformOptions { 12 | const { environment, hash, relativePath } = options; 13 | 14 | switch (environment) { 15 | case ENVIRONMENTS.CLIENT: 16 | return { 17 | generator: new ClientLikeGenerator({ 18 | hash, 19 | relativePath, 20 | registerServerReferenceSource: "react-just/client", 21 | }), 22 | treeshakeImplementation: true, 23 | }; 24 | case ENVIRONMENTS.FIZZ_NODE: 25 | return { 26 | generator: new ClientLikeGenerator({ 27 | hash, 28 | relativePath, 29 | registerServerReferenceSource: "react-just/fizz.node", 30 | }), 31 | treeshakeImplementation: true, 32 | }; 33 | case ENVIRONMENTS.FLIGHT_NODE: 34 | return { 35 | generator: new FlightGenerator({ 36 | hash, 37 | relativePath, 38 | registerServerReferenceSource: "react-just/flight.node", 39 | }), 40 | treeshakeImplementation: false, 41 | }; 42 | case ENVIRONMENTS.SCAN_USE_SERVER_MODULES: 43 | return { 44 | generator: new ClientLikeGenerator({ 45 | hash, 46 | relativePath, 47 | registerServerReferenceSource: "react-just/client", 48 | }), 49 | treeshakeImplementation: true, 50 | }; 51 | default: 52 | throw new Error(`Unexpected environment: ${environment}`); 53 | } 54 | } 55 | 56 | class ClientLikeGenerator extends Generator { 57 | constructor(options: { 58 | hash: boolean; 59 | registerServerReferenceSource: string; 60 | relativePath: string; 61 | }) { 62 | super({ 63 | getRegisterArguments: ({ exportName }) => { 64 | const id = getId(options.relativePath, exportName); 65 | const registerId = options.hash ? hash(id) : id; 66 | return [builders.literal(registerId)]; 67 | }, 68 | registerServerReferenceSource: options.registerServerReferenceSource, 69 | }); 70 | } 71 | } 72 | 73 | class FlightGenerator extends Generator { 74 | constructor(options: { 75 | hash: boolean; 76 | registerServerReferenceSource: string; 77 | relativePath: string; 78 | }) { 79 | super({ 80 | getRegisterArguments: ({ implementationIdentifier, exportName }) => { 81 | const id = getId(options.relativePath, exportName); 82 | const registerId = options.hash ? hash(id) : id; 83 | return [ 84 | builders.identifier(implementationIdentifier), 85 | builders.literal(registerId), 86 | ]; 87 | }, 88 | registerServerReferenceSource: options.registerServerReferenceSource, 89 | }); 90 | } 91 | } 92 | 93 | function getId(relativePath: string, exportName: string) { 94 | return `${relativePath}#${exportName}`; 95 | } 96 | 97 | function hash(id: string) { 98 | // Using deterministic ID to avoid server functions breaking between 99 | // deployments. 100 | return crypto 101 | .createHash("sha256") 102 | .update(id) 103 | .digest() 104 | .subarray(0, HASH_BYTES) 105 | .toString("base64url"); 106 | } 107 | 108 | const HASH_BYTES = 8; 109 | -------------------------------------------------------------------------------- /docs/reference/router/router.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2, 3] 3 | --- 4 | 5 | # Router 6 | 7 | `Router` is a component that handles route matching and renders the appropriate route component for a given URL. 8 | 9 | ```tsx 10 | import { Router, Route } from "@react-just/router"; 11 | import Home from "./components/Home"; 12 | import About from "./components/About"; 13 | 14 | function Routes({ url }: { url: URL }) { 15 | return ( 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | ``` 23 | 24 | :::warning Server Component 25 | The `Router` component must be used as a Server Component. 26 | ::: 27 | 28 | ## Props 29 | 30 | ```tsx 31 | interface RouterProps { 32 | children?: RouteChildren; 33 | url: URL; 34 | } 35 | 36 | type RouteChildren = 37 | | Iterable 38 | | ReactElement 39 | | boolean 40 | | null 41 | | undefined; 42 | ``` 43 | 44 | - `children` (optional): Route components that define the application's routing structure. Can include `Route` components or values resulting from conditional rendering, such as `boolean`, `null`, or `undefined`. 45 | - `url`: A `URL` object representing the current request URL. Typically created from the `url` property of the `req` prop in the App Component. 46 | 47 | ## Examples 48 | 49 | ### Basic Routing with Fallback Route 50 | 51 | ```tsx 52 | import { Router, Route } from "@react-just/router"; 53 | import { request } from "react-just/server"; 54 | import About from "./components/About"; 55 | import Contact from "./components/Contact"; 56 | import Home from "./components/Home"; 57 | import NotFound from "./components/NotFound"; 58 | 59 | function AppRoutes() { 60 | const { url } = request(); 61 | 62 | return ( 63 | 64 | 65 | 66 | 67 | 68 | 69 | ); 70 | } 71 | ``` 72 | 73 | :::info Fallback Route 74 | `*splat` matches any unmatched path and passes it as a parameter named `splat`. 75 | ::: 76 | 77 | ### Nested Routing with Parameters 78 | 79 | ```tsx 80 | import { Router, Route } from "@react-just/router"; 81 | import AdminLayout from "./components/AdminLayout"; 82 | import FileExplorer from "./components/FileExplorer"; 83 | import UserProfile from "./components/UserProfile"; 84 | import UsersList from "./components/UsersList"; 85 | 86 | function Routes({ url }: { url: URL }) { 87 | return ( 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | ); 96 | } 97 | ``` 98 | 99 | ### Conditional Routes 100 | 101 | ```tsx 102 | import { Router, Route } from "@react-just/router"; 103 | import AdminDashboard from "./components/AdminDashboard"; 104 | import Home from "./components/Home"; 105 | import UserDashboard from "./components/UserDashboard"; 106 | 107 | function Routes({ url, user }: { url: URL; user: User }) { 108 | return ( 109 | 110 | 111 | {user.isAdmin && } 112 | 113 | 114 | ); 115 | } 116 | ``` 117 | -------------------------------------------------------------------------------- /docs/guide/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2, 3] 3 | --- 4 | 5 | # Installation 6 | 7 | ## Template Installation 8 | 9 | Start from a template for JavaScript or TypeScript. Replace `my-project` with your desired directory name. 10 | 11 | ### JavaScript 12 | 13 | ::: code-group 14 | 15 | ```bash [npm] 16 | $ npx degit almadoro/react-just/templates/node-js my-project 17 | ``` 18 | 19 | ```bash [pnpm] 20 | $ pnpx degit almadoro/react-just/templates/node-js my-project 21 | ``` 22 | 23 | ```bash [bun] 24 | $ bunx degit almadoro/react-just/templates/node-js my-project 25 | ``` 26 | 27 | ::: 28 | 29 | ### TypeScript 30 | 31 | ::: code-group 32 | 33 | ```bash [npm] 34 | $ npx degit almadoro/react-just/templates/node-ts my-project 35 | ``` 36 | 37 | ```bash [pnpm] 38 | $ pnpx degit almadoro/react-just/templates/node-ts my-project 39 | ``` 40 | 41 | ```bash [bun] 42 | $ bunx degit almadoro/react-just/templates/node-ts my-project 43 | ``` 44 | 45 | ::: 46 | 47 | ## Manual Installation 48 | 49 | Follow the steps below to manually install React Just in your project. 50 | 51 | ### Packages Installation 52 | 53 | Install React packages: 54 | 55 | ::: code-group 56 | 57 | ```bash [npm] 58 | $ npm install react@19.1 react-dom@19.1 59 | ``` 60 | 61 | ```bash [pnpm] 62 | $ pnpm add react@19.1 react-dom@19.1 63 | ``` 64 | 65 | ```bash [Bun] 66 | $ bun add react@19.1 react-dom@19.1 67 | ``` 68 | 69 | ::: warning React 19.1.x recommended 70 | The underlying APIs for React Server Components are not yet stable. The [React team warns](https://react.dev/reference/rsc/server-components) that these APIs may change between minor versions. 71 | 72 | React Just has been tested with React 19.1 and should work with any patch version (`19.1.x`). Compatibility with future versions is not guaranteed. 73 | ::: 74 | 75 | Install Vite and React Just as dev dependencies: 76 | 77 | ::: code-group 78 | 79 | ```bash [npm] 80 | $ npm install -D vite@7 react-just 81 | ``` 82 | 83 | ```bash [pnpm] 84 | $ pnpm add -D vite@7 react-just 85 | ``` 86 | 87 | ```bash [Bun] 88 | $ bun add -D vite@7 react-just 89 | ``` 90 | 91 | ::: 92 | 93 | ### TypeScript Configuration (Optional) 94 | 95 | For a minimal TypeScript setup, use the following configuration: 96 | 97 | ```json [tsconfig.json] 98 | { 99 | "compilerOptions": { 100 | "moduleResolution": "bundler", 101 | "module": "esnext", 102 | "jsx": "preserve" 103 | } 104 | } 105 | ``` 106 | 107 | ### Vite Configuration 108 | 109 | Create a Vite config file that uses the React Just plugin: 110 | 111 | ::: code-group 112 | 113 | ```ts [vite.config.ts] 114 | import react from "react-just/vite"; 115 | import { defineConfig } from "vite"; 116 | 117 | export default defineConfig({ 118 | plugins: [react()], 119 | }); 120 | ``` 121 | 122 | ::: 123 | 124 | ### Minimal App Component 125 | 126 | For a working project, you need to add an [App Component](/guide/app-component). Add a minimal version. 127 | 128 | ```tsx [src/index.tsx] 129 | export default function App() { 130 | return ( 131 | 132 | 133 |

Hello, World!

134 | 135 | 136 | ); 137 | } 138 | ``` 139 | 140 | ### Development Server 141 | 142 | Start the development server with the `vite` command. For convenience, add the following script to your `package.json`: 143 | 144 | ```json [package.json] 145 | { 146 | "scripts": { 147 | "dev": "vite" 148 | } 149 | } 150 | ``` 151 | 152 | Then start the development server with: 153 | 154 | ::: code-group 155 | 156 | ```bash [npm] 157 | $ npm run dev 158 | ``` 159 | 160 | ```bash [pnpm] 161 | $ pnpm dev 162 | ``` 163 | 164 | ```bash [Bun] 165 | $ bun dev 166 | ``` 167 | 168 | ::: 169 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/css.ts: -------------------------------------------------------------------------------- 1 | import { DevEnvironment, Plugin } from "vite"; 2 | import { isClientLikeEnvironment, isFlightEnvironment } from "./environments"; 3 | import { invalidateModules } from "./utils"; 4 | 5 | export default function css(): Plugin { 6 | const flightDevEnvironments: DevEnvironment[] = []; 7 | const clientLikeDevEnvironments: DevEnvironment[] = []; 8 | const cssModuleIds = new Set(); 9 | 10 | function removeUnusedModules() { 11 | const pruneUrls = new Set(); 12 | const pruneIds = new Set(); 13 | 14 | for (const flightEnv of flightDevEnvironments) { 15 | for (const [ 16 | moduleId, 17 | flightModule, 18 | ] of flightEnv.moduleGraph.idToModuleMap.entries()) { 19 | if (!isCssModule(moduleId)) continue; 20 | 21 | const moduleUrl = flightModule.url; 22 | if (!moduleUrl) continue; 23 | 24 | const isFlightUnused = flightModule.importers.size === 0; 25 | 26 | const isUnused = isFlightUnused && isClientUnused(moduleId); 27 | 28 | if (isUnused) { 29 | pruneUrls.add(moduleUrl); 30 | pruneIds.add(moduleId); 31 | cssModuleIds.delete(moduleId); 32 | // Invalidating on the environment forces a transform, thus adding the 33 | // module back to the css module ids, in case it's referenced again. 34 | flightEnv.moduleGraph.invalidateModule(flightModule); 35 | } 36 | } 37 | } 38 | 39 | for (const environment of clientLikeDevEnvironments) { 40 | environment.hot.send({ type: "prune", paths: Array.from(pruneUrls) }); 41 | // Invalidating on client environments forces browsers to re-import 42 | // pruned modules when they are referenced again. 43 | invalidateModules(environment, ...pruneIds); 44 | } 45 | } 46 | 47 | function isClientUnused(moduleId: string) { 48 | for (const clientEnv of clientLikeDevEnvironments) { 49 | const clientModule = clientEnv.moduleGraph.getModuleById(moduleId); 50 | if (!clientModule) continue; 51 | 52 | for (const importer of clientModule.importers) { 53 | if (importer.id !== RESOLVED_CSS_MODULES) { 54 | return false; 55 | } 56 | } 57 | } 58 | 59 | return true; 60 | } 61 | 62 | return { 63 | name: "react-just:css", 64 | sharedDuringBuild: true, 65 | configureServer(server) { 66 | for (const environment of Object.values(server.environments)) { 67 | if (isFlightEnvironment(environment.name)) 68 | flightDevEnvironments.push(environment); 69 | else if (isClientLikeEnvironment(environment.name)) 70 | clientLikeDevEnvironments.push(environment); 71 | } 72 | }, 73 | resolveId(id) { 74 | if (id === CSS_MODULES) return RESOLVED_CSS_MODULES; 75 | }, 76 | load(id) { 77 | if (isFlightEnvironment(this.environment.name) && isCssModule(id)) 78 | cssModuleIds.add(id); 79 | 80 | if (id === RESOLVED_CSS_MODULES) { 81 | removeUnusedModules(); 82 | return getCssModulesCode(cssModuleIds); 83 | } 84 | }, 85 | }; 86 | } 87 | 88 | export const CSS_MODULES = "/virtual:react-just/css-modules"; 89 | export const RESOLVED_CSS_MODULES = "\0" + CSS_MODULES; 90 | 91 | function getCssModulesCode(ids: Iterable) { 92 | return Array.from(ids) 93 | .map((id) => `import "${id}";`) 94 | .join("\n"); 95 | } 96 | 97 | function isCssModule(id: string) { 98 | return CSS_EXTENSIONS_RE.test(id); 99 | } 100 | 101 | // Taken from: https://github.com/vitejs/vite/blob/main/packages/vite/src/node/constants.ts#L92 102 | const CSS_EXTENSIONS_RE = 103 | /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)($|\?)/; 104 | -------------------------------------------------------------------------------- /packages/react-just/src/flight/node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DecodeReplyOptions, 3 | RenderToPipeableStreamOptions, 4 | } from "@/types/flight.node"; 5 | import { 6 | PipeableStream, 7 | ReactClientValue, 8 | ReactFormState, 9 | } from "@/types/shared"; 10 | import busboy from "busboy"; 11 | import { IncomingMessage } from "node:http"; 12 | import { 13 | decodeAction as baseDecodeAction, 14 | decodeFormState as baseDecodeFormState, 15 | decodeReply as baseDecodeReply, 16 | registerClientReference as baseRegisterClientReference, 17 | registerServerReference as baseRegisterServerReference, 18 | renderToPipeableStream as baseRenderToPipeableStream, 19 | createTemporaryReferenceSet, 20 | decodeReplyFromBusboy, 21 | } from "react-server-dom-webpack/server.node"; 22 | import { runWithContext } from "../async-store/node"; 23 | import { 24 | IMPLEMENTATION_EXPORT_NAME, 25 | registerImplementation, 26 | } from "../implementations"; 27 | 28 | export { createTemporaryReferenceSet }; 29 | 30 | export function decodeAction(body: FormData): Promise<() => T> | null { 31 | return baseDecodeAction(body, serverMap); 32 | } 33 | 34 | export function decodeFormState( 35 | actionResult: S, 36 | body: FormData, 37 | ): Promise { 38 | return baseDecodeFormState(actionResult, body, serverMap); 39 | } 40 | 41 | export async function decodeReply( 42 | req: IncomingMessage, 43 | options: DecodeReplyOptions, 44 | ): Promise { 45 | const contentType = req.headers["content-type"]; 46 | 47 | if (contentType?.startsWith("multipart/form-data")) { 48 | const bb = busboy({ headers: req.headers }); 49 | req.pipe(bb); 50 | const payload = await decodeReplyFromBusboy(bb, serverMap, { 51 | temporaryReferences: options.temporaryReferences, 52 | }); 53 | return payload; 54 | } 55 | 56 | const chunks: Buffer[] = []; 57 | await new Promise((resolve, reject) => { 58 | req.on("data", (chunk) => chunks.push(chunk)); 59 | req.on("end", resolve); 60 | req.on("error", reject); 61 | }); 62 | const buffer = Buffer.concat(chunks); 63 | const payload = await baseDecodeReply( 64 | buffer.toString("utf-8"), 65 | serverMap, 66 | { temporaryReferences: options.temporaryReferences }, 67 | ); 68 | return payload; 69 | } 70 | 71 | /* @__NO_SIDE_EFFECTS__ */ 72 | export function registerClientReference( 73 | moduleId: string | number, 74 | exportName: string | number, 75 | ): unknown { 76 | return baseRegisterClientReference( 77 | {}, 78 | moduleId.toString(), 79 | exportName.toString(), 80 | ); 81 | } 82 | 83 | export function registerServerReference( 84 | implementation: T, 85 | id: string, 86 | ): T { 87 | if (!(implementation instanceof AsyncFunction)) 88 | throw new Error(`Server functions must be async functions: ${id}`); 89 | 90 | registerImplementation(implementation, id); 91 | 92 | return baseRegisterServerReference(implementation, id, null); 93 | } 94 | 95 | export function renderToPipeableStream( 96 | value: ReactClientValue, 97 | options: RenderToPipeableStreamOptions, 98 | ): PipeableStream { 99 | return baseRenderToPipeableStream(value, clientMap, { 100 | temporaryReferences: options.temporaryReferences, 101 | }); 102 | } 103 | 104 | export { runWithContext }; 105 | 106 | const AsyncFunction = (async () => {}).constructor; 107 | 108 | const clientMap = new Proxy( 109 | {}, 110 | { 111 | get(_, prop) { 112 | if (typeof prop !== "string") return null; 113 | return { 114 | id: prop, 115 | chunks: [], 116 | name: IMPLEMENTATION_EXPORT_NAME, 117 | async: false, 118 | }; 119 | }, 120 | }, 121 | ); 122 | 123 | const serverMap = new Proxy( 124 | {}, 125 | { 126 | get(_, prop) { 127 | if (typeof prop !== "string") return null; 128 | return { 129 | id: prop, 130 | chunks: [], 131 | name: IMPLEMENTATION_EXPORT_NAME, 132 | async: false, 133 | }; 134 | }, 135 | }, 136 | ); 137 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-client/transform/generator.ts: -------------------------------------------------------------------------------- 1 | import { Expression, Node, Program } from "estree"; 2 | import { builders } from "estree-toolkit"; 3 | 4 | export default class Generator { 5 | constructor(private options: GeneratorOptions) {} 6 | 7 | /** 8 | * Creates a node with the following structure: 9 | * 10 | * ```js 11 | * import { registerClientReference as $$registerClientReference$$ } from "..."; 12 | * ``` 13 | */ 14 | public createRegisterFunctionImport() { 15 | return builders.importDeclaration( 16 | [ 17 | builders.importSpecifier( 18 | builders.identifier(REGISTER_CLIENT_REFERENCE_SOURCE_IDENTIFIER), 19 | builders.identifier(REGISTER_CLIENT_REFERENCE_IDENTIFIER), 20 | ), 21 | ], 22 | builders.literal(this.options.registerClientReferenceSource), 23 | ); 24 | } 25 | 26 | /** 27 | * Create nodes with the following structure: 28 | * 29 | * ```js 30 | * const $$Ref$$exportName = $$registerClientReference$$(...); 31 | * export { $$Ref$$exportName as exportName }; 32 | * ``` 33 | */ 34 | public createRegisterAndExportReference( 35 | exportName: string, 36 | implementationIdentifier: string, 37 | ) { 38 | const referenceIdentifier = REFERENCE_PREFIX + exportName; 39 | 40 | const callExpression = builders.callExpression( 41 | builders.identifier(REGISTER_CLIENT_REFERENCE_IDENTIFIER), 42 | this.options.getRegisterArguments({ 43 | exportName, 44 | implementationIdentifier, 45 | }), 46 | ); 47 | 48 | return [ 49 | builders.variableDeclaration("const", [ 50 | builders.variableDeclarator( 51 | builders.identifier(referenceIdentifier), 52 | callExpression, 53 | ), 54 | ]), 55 | builders.exportNamedDeclaration(null, [ 56 | builders.exportSpecifier( 57 | builders.identifier(referenceIdentifier), 58 | builders.identifier(exportName), 59 | ), 60 | ]), 61 | ]; 62 | } 63 | 64 | /** 65 | * Treeshakes the implementation of the module. Leaves only the necessary 66 | * code to register the client references. 67 | */ 68 | public createTreeshakedBody(body: Program["body"]) { 69 | const newBody: Program["body"] = []; 70 | 71 | for (const node of body) { 72 | if ( 73 | this.isExportDeclaration(node) || 74 | this.isRegisterReferenceDeclaration(node) || 75 | this.isRegisterFunctionImport(node) 76 | ) { 77 | newBody.push(node); 78 | } 79 | } 80 | 81 | return newBody; 82 | } 83 | 84 | private isExportDeclaration(node: Node) { 85 | return node.type === "ExportNamedDeclaration"; 86 | } 87 | 88 | private isRegisterReferenceDeclaration(node: Node) { 89 | if (node.type !== "VariableDeclaration") return false; 90 | 91 | const [declaration] = node.declarations; 92 | 93 | return ( 94 | node.declarations.length === 1 && 95 | declaration.type === "VariableDeclarator" && 96 | declaration.id.type === "Identifier" && 97 | declaration.id.name.startsWith(REFERENCE_PREFIX) 98 | ); 99 | } 100 | 101 | private isRegisterFunctionImport(node: Node) { 102 | return ( 103 | node.type === "ImportDeclaration" && 104 | node.source.value === this.options.registerClientReferenceSource 105 | ); 106 | } 107 | } 108 | 109 | const REFERENCE_PREFIX = "$$Ref$$"; 110 | const REGISTER_CLIENT_REFERENCE_IDENTIFIER = "$$registerClientReference$$"; 111 | const REGISTER_CLIENT_REFERENCE_SOURCE_IDENTIFIER = "registerClientReference"; 112 | 113 | type GeneratorOptions = { 114 | /** 115 | * A function that returns the arguments to be passed to the 116 | * `registerClientReference` function call. 117 | */ 118 | getRegisterArguments: GetRegisterArgumentsFn; 119 | /** 120 | * The source of the `registerClientReference` function. 121 | */ 122 | registerClientReferenceSource: string; 123 | }; 124 | 125 | type GetRegisterArgumentsFn = (ctx: { 126 | exportName: string; 127 | implementationIdentifier: string; 128 | }) => Expression[]; 129 | -------------------------------------------------------------------------------- /packages/react-just/src/vite/use-server/transform/generator.ts: -------------------------------------------------------------------------------- 1 | import { Expression, Node, Program } from "estree"; 2 | import { builders } from "estree-toolkit"; 3 | 4 | export default class Generator { 5 | constructor(private options: GeneratorOptions) {} 6 | 7 | /** 8 | * Creates a node with the following structure: 9 | * 10 | * ```js 11 | * import { registerServerReference as $$registerServerReference$$ } from "..."; 12 | * ``` 13 | */ 14 | public createRegisterFunctionImport() { 15 | return builders.importDeclaration( 16 | [ 17 | builders.importSpecifier( 18 | builders.identifier(REGISTER_SERVER_REFERENCE_SOURCE_IDENTIFIER), 19 | builders.identifier(REGISTER_SERVER_REFERENCE_IDENTIFIER), 20 | ), 21 | ], 22 | builders.literal(this.options.registerServerReferenceSource), 23 | ); 24 | } 25 | 26 | /** 27 | * Create nodes with the following structure: 28 | * 29 | * ```js 30 | * const $$Ref$$exportName = $$registerServerReference$$(...); 31 | * export { $$Ref$$exportName as exportName }; 32 | * ``` 33 | */ 34 | public createRegisterAndExportReference( 35 | exportName: string, 36 | implementationIdentifier: string, 37 | ) { 38 | const referenceIdentifier = REFERENCE_PREFIX + exportName; 39 | 40 | const callExpression = builders.callExpression( 41 | builders.identifier(REGISTER_SERVER_REFERENCE_IDENTIFIER), 42 | this.options.getRegisterArguments({ 43 | exportName, 44 | implementationIdentifier, 45 | }), 46 | ); 47 | 48 | return [ 49 | builders.variableDeclaration("const", [ 50 | builders.variableDeclarator( 51 | builders.identifier(referenceIdentifier), 52 | callExpression, 53 | ), 54 | ]), 55 | builders.exportNamedDeclaration(null, [ 56 | builders.exportSpecifier( 57 | builders.identifier(referenceIdentifier), 58 | builders.identifier(exportName), 59 | ), 60 | ]), 61 | ]; 62 | } 63 | 64 | /** 65 | * Treeshakes the implementation of the module. Leaves only the necessary 66 | * code to register the server references. 67 | */ 68 | public createTreeshakedBody(body: Program["body"]) { 69 | const newBody: Program["body"] = []; 70 | 71 | for (const node of body) { 72 | if ( 73 | this.isExportDeclaration(node) || 74 | this.isRegisterReferenceDeclaration(node) || 75 | this.isRegisterFunctionImport(node) 76 | ) { 77 | newBody.push(node); 78 | } 79 | } 80 | 81 | return newBody; 82 | } 83 | 84 | private isExportDeclaration(node: Node) { 85 | return node.type === "ExportNamedDeclaration"; 86 | } 87 | 88 | private isRegisterReferenceDeclaration(node: Node) { 89 | if (node.type !== "VariableDeclaration") return false; 90 | 91 | const [declaration] = node.declarations; 92 | 93 | return ( 94 | node.declarations.length === 1 && 95 | declaration.type === "VariableDeclarator" && 96 | declaration.id.type === "Identifier" && 97 | declaration.id.name.startsWith(REFERENCE_PREFIX) 98 | ); 99 | } 100 | 101 | private isRegisterFunctionImport(node: Node) { 102 | return ( 103 | node.type === "ImportDeclaration" && 104 | node.source.value === this.options.registerServerReferenceSource 105 | ); 106 | } 107 | } 108 | 109 | const REFERENCE_PREFIX = "$$Ref$$"; 110 | const REGISTER_SERVER_REFERENCE_IDENTIFIER = "$$registerServerReference$$"; 111 | const REGISTER_SERVER_REFERENCE_SOURCE_IDENTIFIER = "registerServerReference"; 112 | 113 | type GeneratorOptions = { 114 | /** 115 | * A function that returns the arguments to be passed to the 116 | * `registerServerReference` function call. 117 | */ 118 | getRegisterArguments: GetRegisterArgumentsFn; 119 | /** 120 | * The source of the `registerServerReference` function. 121 | */ 122 | registerServerReferenceSource: string; 123 | }; 124 | 125 | type GetRegisterArgumentsFn = (ctx: { 126 | exportName: string; 127 | implementationIdentifier: string; 128 | }) => Expression[]; 129 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-server/transform/exports.test.ts: -------------------------------------------------------------------------------- 1 | import baseTransform, { TransformOptions } from "@/vite/use-server/transform"; 2 | import Generator from "@/vite/use-server/transform/generator"; 3 | import { generate } from "astring"; 4 | import { builders } from "estree-toolkit"; 5 | import fs from "node:fs/promises"; 6 | import path from "node:path"; 7 | import { parseAst } from "vite"; 8 | import { describe, expect, test } from "vitest"; 9 | 10 | // flight-like environment 11 | const noTreeshakeEnv: TransformOptions = { 12 | generator: new Generator({ 13 | getRegisterArguments: ({ exportName, implementationIdentifier }) => [ 14 | builders.identifier(implementationIdentifier), 15 | builders.literal("action:" + exportName), 16 | builders.literal(exportName), 17 | ], 18 | registerServerReferenceSource: "react-just/no-treeshake", 19 | }), 20 | treeshakeImplementation: false, 21 | }; 22 | 23 | // client-like environment 24 | const treeshakeEnv: TransformOptions = { 25 | generator: new Generator({ 26 | getRegisterArguments: ({ exportName }) => [ 27 | builders.literal("action:" + exportName), 28 | ], 29 | registerServerReferenceSource: "react-just/treeshake", 30 | }), 31 | treeshakeImplementation: true, 32 | }; 33 | 34 | describe( 35 | "transforms exports on a no-treeshake environment", 36 | exportsTests(noTreeshakeEnv), 37 | ); 38 | 39 | describe( 40 | "transforms exports on a treeshake environment", 41 | exportsTests(treeshakeEnv), 42 | ); 43 | 44 | function exportsTests(options: TransformOptions) { 45 | return () => { 46 | describe("export default declarations", () => { 47 | test( 48 | "class declarations", 49 | transformTest("export-default-declaration/class-declaration.js"), 50 | ); 51 | 52 | test( 53 | "function declarations", 54 | transformTest("export-default-declaration/function-declaration.js"), 55 | ); 56 | 57 | test( 58 | "identifier", 59 | transformTest("export-default-declaration/identifier.js"), 60 | ); 61 | 62 | test( 63 | "value declarations", 64 | transformTest("export-default-declaration/value-declaration.js"), 65 | ); 66 | }); 67 | 68 | describe("export named declarations", () => { 69 | test( 70 | "array pattern", 71 | transformTest("export-named-declaration/array-pattern.js"), 72 | ); 73 | 74 | test( 75 | "assignment pattern", 76 | transformTest("export-named-declaration/assignment-pattern.js"), 77 | ); 78 | 79 | test("class", transformTest("export-named-declaration/class.js")); 80 | 81 | test( 82 | "identifier", 83 | transformTest("export-named-declaration/identifier.js"), 84 | ); 85 | 86 | test("function", transformTest("export-named-declaration/function.js")); 87 | 88 | test( 89 | "object pattern", 90 | transformTest("export-named-declaration/object-pattern.js"), 91 | ); 92 | 93 | test( 94 | "rest element", 95 | transformTest("export-named-declaration/rest-element.js"), 96 | ); 97 | }); 98 | 99 | test( 100 | "export named from source", 101 | transformTest("export-named-from-source.js"), 102 | ); 103 | 104 | test( 105 | "export named specifiers", 106 | transformTest("export-named-specifiers.js"), 107 | ); 108 | 109 | test("throws when there is an export all declaration", () => { 110 | expect(() => 111 | transform("'use server'; export * from 'pkg';"), 112 | ).toThrowError(); 113 | }); 114 | }; 115 | 116 | function transformTest(filepath: string) { 117 | return async () => { 118 | const code = await fs.readFile( 119 | path.resolve(import.meta.dirname, "fixtures", filepath), 120 | "utf-8", 121 | ); 122 | 123 | const output = transform(code); 124 | 125 | expect(output).toMatchSnapshot(); 126 | }; 127 | } 128 | 129 | function transform(code: string) { 130 | const ast = parseAst(code); 131 | 132 | baseTransform(ast, options); 133 | 134 | return generate(ast); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /docs/reference/router/route.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2, 3] 3 | --- 4 | 5 | # Route 6 | 7 | `Route` is a declarative component that defines route patterns and their corresponding components inside a [Router](/reference/router/router). 8 | 9 | ```tsx 10 | import { Route, Router } from "@react-just/router"; 11 | import Home from "./Home"; 12 | import NotFound from "./NotFound"; 13 | 14 | function Routes({ url }: { url: URL }) { 15 | return ( 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | ``` 23 | 24 | :::info 25 | `Route` components are not rendered directly. Instead, the `Router` component processes them to build the routing table. 26 | ::: 27 | 28 | ## Props 29 | 30 | ```tsx 31 | interface RouteProps { 32 | children?: RouteChildren; 33 | component: ComponentType; 34 | path: string; 35 | } 36 | 37 | type RouteChildren = 38 | | Iterable 39 | | ReactElement 40 | | boolean 41 | | null 42 | | undefined; 43 | ``` 44 | 45 | - `children` (optional): Nested route definitions for creating route hierarchies. 46 | - `component`: The React component rendered when the route matches. The component will receive [RouteComponentProps](/reference/router/route-component-props). 47 | - `path`: The URL pattern to match against. Supports static segments, path and wildcard parameters, and optional segments. 48 | 49 | :::info Leading Slash 50 | The leading slash is ignored, so `path="about"` and `path="/about"` are equivalent. For consistency, it's recommended to always include the leading slash. 51 | ::: 52 | 53 | ## Path Patterns 54 | 55 | ### Static Paths 56 | 57 | ```tsx 58 | 59 | 60 | 61 | ``` 62 | 63 | ### Path Parameters 64 | 65 | Use `:paramName` to capture dynamic segments: 66 | 67 | ```tsx 68 | 69 | 70 | ``` 71 | 72 | Path parameters are available via the `params` prop in the component or the [`useParams`](/reference/router/use-params) hook in a client module. 73 | 74 | Parameter name must be a valid [identifier](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers) containing only letters, numbers, or underscores `_`. 75 | 76 | ### Optional Parameters 77 | 78 | Add `?` to make a parameter optional: 79 | 80 | ```tsx 81 | 82 | ``` 83 | 84 | This matches both `/products/electronics` and `/products/electronics/phones`. 85 | 86 | ### Wildcard Parameters 87 | 88 | Use `*paramName` to capture the rest of the path: 89 | 90 | ```tsx 91 | 92 | 93 | ``` 94 | 95 | Wildcard parameters capture all remaining path segments as an array. You can access them through the `params` prop in the component or the [`useParams`](/reference/router/use-params) hook in a client module. 96 | 97 | Parameter name must be a valid [identifier](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers) containing only letters, numbers, or underscores `_`. 98 | 99 | :::warning Invalid Patterns 100 | 101 | The `Router` throws an error if it encounters an invalid pattern: 102 | 103 | - Empty path segments: `"/users//profile"`. 104 | - Trailing slashes: `"/about/"`. 105 | - Invalid parameter names: `"/:user-id"`. 106 | ::: 107 | 108 | ## Nesting 109 | 110 | Routes can be nested to define hierarchical routing structures: 111 | 112 | ```tsx 113 | 114 | 115 | 116 | 117 | 118 | ``` 119 | 120 | :::warning No Children for Index Routes 121 | Index routes (`path="/"`) cannot have children. Otherwise, the `Router` will throw an error. 122 | ::: 123 | -------------------------------------------------------------------------------- /packages/react-just/__tests__/use-client/transform/exports.test.ts: -------------------------------------------------------------------------------- 1 | import transform, { TransformOptions } from "@/vite/use-client/transform"; 2 | import Generator from "@/vite/use-client/transform/generator"; 3 | import { generate } from "astring"; 4 | import { builders } from "estree-toolkit"; 5 | import fs from "node:fs/promises"; 6 | import path from "node:path"; 7 | import { parseAstAsync } from "vite"; 8 | import { describe, expect, test } from "vitest"; 9 | 10 | const MODULE_ID = "module.js"; 11 | 12 | // client-like environment 13 | const noTreeshakeEnv: TransformOptions = { 14 | generator: new Generator({ 15 | getRegisterArguments: ({ exportName, implementationIdentifier }) => [ 16 | builders.identifier(implementationIdentifier), 17 | builders.literal(MODULE_ID), 18 | builders.literal(exportName), 19 | ], 20 | registerClientReferenceSource: "react-just/no-treeshake", 21 | }), 22 | treeshakeImplementation: false, 23 | }; 24 | 25 | // flight-like environment 26 | const treeshakeEnv: TransformOptions = { 27 | generator: new Generator({ 28 | getRegisterArguments: ({ exportName }) => [ 29 | builders.literal(MODULE_ID), 30 | builders.literal(exportName), 31 | ], 32 | registerClientReferenceSource: "react-just/treeshake", 33 | }), 34 | treeshakeImplementation: true, 35 | }; 36 | 37 | describe( 38 | "transforms exports on a no-treeshake environment", 39 | exportsTests(noTreeshakeEnv), 40 | ); 41 | describe( 42 | "transforms exports on a treeshake environment", 43 | exportsTests(treeshakeEnv), 44 | ); 45 | 46 | function exportsTests(options: TransformOptions) { 47 | return () => { 48 | describe("export default declarations", () => { 49 | test( 50 | "class declarations", 51 | transformTest("export-default-declaration/class-declaration.js"), 52 | ); 53 | 54 | test( 55 | "function declarations", 56 | transformTest("export-default-declaration/function-declaration.js"), 57 | ); 58 | 59 | test( 60 | "identifier", 61 | transformTest("export-default-declaration/identifier.js"), 62 | ); 63 | 64 | test( 65 | "value declarations", 66 | transformTest("export-default-declaration/value-declaration.js"), 67 | ); 68 | }); 69 | 70 | describe("export named declarations", () => { 71 | test( 72 | "array pattern", 73 | transformTest("export-named-declaration/array-pattern.js"), 74 | ); 75 | 76 | test( 77 | "assignment pattern", 78 | transformTest("export-named-declaration/assignment-pattern.js"), 79 | ); 80 | 81 | test("class", transformTest("export-named-declaration/class.js")); 82 | 83 | test( 84 | "identifier", 85 | transformTest("export-named-declaration/identifier.js"), 86 | ); 87 | 88 | test("function", transformTest("export-named-declaration/function.js")); 89 | 90 | test( 91 | "object pattern", 92 | transformTest("export-named-declaration/object-pattern.js"), 93 | ); 94 | 95 | test( 96 | "rest element", 97 | transformTest("export-named-declaration/rest-element.js"), 98 | ); 99 | }); 100 | 101 | test( 102 | "export named from source", 103 | transformTest("export-named-from-source.js"), 104 | ); 105 | 106 | test( 107 | "export named specifiers", 108 | transformTest("export-named-specifiers.js"), 109 | ); 110 | 111 | test("throws when there is an export all declaration", async () => { 112 | const code = "'use client'; export * from 'pkg'"; 113 | 114 | const program = await parseAstAsync(code); 115 | 116 | expect(() => transform(program, options)).toThrowError(); 117 | }); 118 | }; 119 | 120 | function transformTest(filepath: string) { 121 | return async () => { 122 | const code = await fs.readFile( 123 | path.resolve(import.meta.dirname, "fixtures", filepath), 124 | "utf-8", 125 | ); 126 | 127 | const program = await parseAstAsync(code); 128 | 129 | transform(program, options); 130 | 131 | const output = generate(program); 132 | 133 | expect(output).toMatchSnapshot(); 134 | }; 135 | } 136 | } 137 | --------------------------------------------------------------------------------
19 |

Hello, React Just!

20 |
21 | React Just 26 | 27 |
28 |
29 |

30 | This is a client counter. If you refresh the page, the count 31 | will reset. 32 |

33 | 34 |
35 |
36 |

37 | This is a server counter. If you refresh the page, the count 38 | will not reset. 39 |

40 | 41 |
42 |
43 |

44 | Click on the React Just logo or{" "} 45 | 46 | here 47 | {" "} 48 | to learn more 49 |

50 |
51 |

52 | Powered by{" "} 53 | 54 | Vite 55 | 56 |

57 | 58 | Vite 59 | 60 |
61 |