From 4e31d1892410317b370f3db8d3e65ebc9ce0d5bf Mon Sep 17 00:00:00 2001 From: Willy Brauner Date: Sat, 7 Sep 2024 17:55:02 +0200 Subject: [PATCH] feat: create low-router-preact (#43) * feat: create low-router-preact * feat: improve I18n for size * manage imports * perf: improve by removing if possible * comment logs * feat: Docs * struct: Move examples in there repositiories * repo: Update size limit * fix: preact ssr example sandbox * feat: Install stackBlitz bot * feat: Remove stackBlitz app * feat: Remove jsx * externalize deps * Create type patch * feat: Init react version, but react types are not good * wip * wip * fix: Remove react package * minor * move docs * rename example * update packages * upgrade all packages * update ci * update changeset * add changeset --- .changeset/config.json | 4 +- .changeset/tough-socks-marry.md | 90 + .codesandbox/ci.json | 13 +- .github/workflows/release.yml | 10 +- README.md | 377 +- package.json | 32 +- packages/low-router-preact/README.md | 443 + .../examples/example-preact-ssr/.env | 1 + .../examples/example-preact-ssr/.gitignore | 12 + .../example-preact-ssr/config/config.js | 18 + .../config/vite-plugins/vite-custom-logger.ts | 39 + .../examples/example-preact-ssr/index.html | 16 + .../examples/example-preact-ssr/package.json | 61 + .../examples/example-preact-ssr/server.dev.ts | 74 + .../example-preact-ssr/server.prod.ts | 69 + .../src/components/App/App.module.scss | 6 + .../src/components/App/App.tsx | 33 + .../src/core/server-utils/ManifestParser.ts | 169 + .../src/core/server-utils/RawScript.tsx | 16 + .../src/core/server-utils/ScriptsTags.tsx | 23 + .../src/helpers/defaultTransitions.ts | 34 + .../example-preact-ssr/src/index-client.tsx | 46 + .../example-preact-ssr/src/index-server.tsx | 61 + .../example-preact-ssr/src/index.scss | 26 + .../example-preact-ssr/src/pages/AAPage.tsx | 38 + .../src/pages/AboutPage.tsx | 48 + .../example-preact-ssr/src/pages/BBPage.tsx | 48 + .../example-preact-ssr/src/pages/BarPage.tsx | 49 + .../example-preact-ssr/src/pages/CCPage.tsx | 38 + .../example-preact-ssr/src/pages/DDPage.tsx | 37 + .../example-preact-ssr/src/pages/FooPage.tsx | 37 + .../example-preact-ssr/src/pages/HomePage.tsx | 49 + .../src/pages/HomeSubAPage.tsx | 37 + .../src/pages/HomeSubBPage.tsx | 37 + .../src/pages/NotFoundPage.tsx | 33 + .../example-preact-ssr/src/pages/WorkPage.tsx | 42 + .../examples/example-preact-ssr/src/routes.ts | 134 + .../example-preact-ssr}/src/vite-env.d.ts | 0 .../examples/example-preact-ssr/tsconfig.json | 37 + .../example-preact-ssr/vite.config.ts | 99 + .../vite.ssr-scripts.config.ts | 50 + packages/low-router-preact/package.json | 49 + .../low-router-preact/src/components/Link.ts | 66 + .../src/components/Router.ts | 288 + .../low-router-preact/src/components/Stack.ts | 150 + .../src/core/composeUrlByRouteName.ts | 46 + .../src/core/getStaticPropsFromUrl.ts | 58 + .../low-router-preact/src/core/setLocation.ts | 25 + .../low-router-preact/src/core/useCache.ts | 29 + .../src/helpers/addLocaleToUrl.ts | 23 + .../src/helpers/joinPaths.ts | 11 + .../src/helpers/safeMergeObjects.ts | 13 + .../src/hooks/useCreateRouter.ts | 24 + .../low-router-preact/src/hooks/useRouter.ts | 4 + packages/low-router-preact/src/index.ts | 20 + packages/low-router-preact/src/preact-deps.ts | 21 + packages/low-router-preact/src/router.d.ts | 18 + .../low-router-preact/src/services/I18n.ts | 320 + packages/low-router-preact/tsconfig.json | 15 + packages/low-router-preact/tsup.config.ts | 51 + packages/low-router/README.md | 391 +- .../low-router/examples}/.gitkeep | 0 .../examples}/basic-resolve-sync/.gitignore | 0 .../examples}/basic-resolve-sync/about.html | 0 .../examples}/basic-resolve-sync/index.html | 0 .../examples}/basic-resolve-sync/package.json | 4 +- .../basic-resolve-sync/public/vite.svg | 0 .../examples}/basic-resolve-sync/src/index.ts | 0 .../basic-resolve-sync/src/style.css | 0 .../basic-resolve-sync}/src/vite-env.d.ts | 0 .../basic-resolve-sync/tsconfig.json | 0 .../basic-resolve-sync/vite.config.ts | 0 .../low-router/examples}/basic/.gitignore | 0 .../low-router/examples}/basic/about.html | 0 .../low-router/examples}/basic/index.html | 0 .../low-router/examples}/basic/package.json | 4 +- .../examples}/basic/public/vite.svg | 0 .../low-router/examples}/basic/src/index.ts | 0 .../low-router/examples}/basic/src/style.css | 0 .../examples/basic}/src/vite-env.d.ts | 0 .../low-router/examples}/basic/tsconfig.json | 0 .../low-router/examples}/basic/vite.config.ts | 0 .../low-router/examples}/compose/.gitignore | 0 .../low-router/examples}/compose/about.html | 0 .../low-router/examples}/compose/contact.html | 0 .../low-router/examples}/compose/index.html | 0 .../low-router/examples}/compose/package.json | 8 +- .../examples}/compose/public/vite.svg | 0 .../examples}/compose/src/components/App.ts | 0 .../compose/src/components/Button.ts | 0 .../compose/src/compose/Component.ts | 0 .../compose/src/helpers/defaultTransition.ts | 0 .../src/helpers/getTranslate3DValues.ts | 0 .../src/helpers/setRandomBackgroundColor.ts | 0 .../examples}/compose/src/index.css | 0 .../low-router/examples}/compose/src/index.ts | 0 .../examples}/compose/src/pages /About.ts | 0 .../examples}/compose/src/pages /AboutFoo.ts | 0 .../examples}/compose/src/pages /Contact.ts | 0 .../examples}/compose/src/pages /Home.ts | 0 .../examples/compose}/src/vite-env.d.ts | 0 .../examples}/compose/tsconfig.json | 0 .../examples}/compose/vite.config.ts | 0 .../custom-path-to-regexp/.gitignore | 0 .../custom-path-to-regexp/about.html | 0 .../custom-path-to-regexp/index.html | 0 .../custom-path-to-regexp/package.json | 6 +- .../custom-path-to-regexp/public/vite.svg | 0 .../custom-path-to-regexp/src/index.ts | 0 .../custom-path-to-regexp/src/style.css | 0 .../custom-path-to-regexp}/src/vite-env.d.ts | 0 .../custom-path-to-regexp/tsconfig.json | 0 .../custom-path-to-regexp/vite.config.ts | 0 .../examples}/react-nested-routes/.gitignore | 0 .../examples}/react-nested-routes/README.md | 0 .../examples}/react-nested-routes/index.html | 0 .../react-nested-routes/package.json | 16 +- .../react-nested-routes/public/vite.svg | 0 .../react-nested-routes/src/assets/react.svg | 0 .../react-nested-routes/src/beeper.ts | 0 .../src/components/app/App.css | 0 .../src/components/app/App.tsx | 0 .../react-nested-routes/src/index.css | 0 .../react-nested-routes/src/index.tsx | 0 .../src/lowRouterReact/BrowserHistory.tsx | 0 .../src/lowRouterReact/Router.tsx | 0 .../src/lowRouterReact/Stack.tsx | 0 .../src/lowRouterReact/useRouter.tsx | 0 .../react-nested-routes/src/pages/A.tsx | 0 .../react-nested-routes/src/pages/About.tsx | 0 .../react-nested-routes/src/pages/Foo.tsx | 0 .../react-nested-routes/src/routes.tsx | 0 .../react-nested-routes/src/vite-env.d.ts | 1 + .../react-nested-routes/tsconfig.json | 0 .../react-nested-routes/tsconfig.node.json | 0 .../react-nested-routes/vite.config.ts | 0 packages/low-router/package.json | 13 +- packages/low-router/tsup.config.ts | 1 + pnpm-lock.yaml | 9307 +++++++++++------ pnpm-workspace.yaml | 2 +- tsconfig.json | 5 - turbo.json | 5 +- 142 files changed, 9727 insertions(+), 3753 deletions(-) create mode 100644 .changeset/tough-socks-marry.md create mode 100644 packages/low-router-preact/README.md create mode 100644 packages/low-router-preact/examples/example-preact-ssr/.env create mode 100644 packages/low-router-preact/examples/example-preact-ssr/.gitignore create mode 100644 packages/low-router-preact/examples/example-preact-ssr/config/config.js create mode 100644 packages/low-router-preact/examples/example-preact-ssr/config/vite-plugins/vite-custom-logger.ts create mode 100644 packages/low-router-preact/examples/example-preact-ssr/index.html create mode 100644 packages/low-router-preact/examples/example-preact-ssr/package.json create mode 100644 packages/low-router-preact/examples/example-preact-ssr/server.dev.ts create mode 100644 packages/low-router-preact/examples/example-preact-ssr/server.prod.ts create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/components/App/App.module.scss create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/components/App/App.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/core/server-utils/ManifestParser.ts create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/core/server-utils/RawScript.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/core/server-utils/ScriptsTags.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/helpers/defaultTransitions.ts create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/index-client.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/index-server.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/index.scss create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/AAPage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/AboutPage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/BBPage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/BarPage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/CCPage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/DDPage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/FooPage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/HomePage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/HomeSubAPage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/HomeSubBPage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/NotFoundPage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/pages/WorkPage.tsx create mode 100644 packages/low-router-preact/examples/example-preact-ssr/src/routes.ts rename {examples/basic-resolve-sync => packages/low-router-preact/examples/example-preact-ssr}/src/vite-env.d.ts (100%) create mode 100644 packages/low-router-preact/examples/example-preact-ssr/tsconfig.json create mode 100644 packages/low-router-preact/examples/example-preact-ssr/vite.config.ts create mode 100644 packages/low-router-preact/examples/example-preact-ssr/vite.ssr-scripts.config.ts create mode 100644 packages/low-router-preact/package.json create mode 100644 packages/low-router-preact/src/components/Link.ts create mode 100644 packages/low-router-preact/src/components/Router.ts create mode 100644 packages/low-router-preact/src/components/Stack.ts create mode 100644 packages/low-router-preact/src/core/composeUrlByRouteName.ts create mode 100644 packages/low-router-preact/src/core/getStaticPropsFromUrl.ts create mode 100644 packages/low-router-preact/src/core/setLocation.ts create mode 100644 packages/low-router-preact/src/core/useCache.ts create mode 100644 packages/low-router-preact/src/helpers/addLocaleToUrl.ts create mode 100644 packages/low-router-preact/src/helpers/joinPaths.ts create mode 100644 packages/low-router-preact/src/helpers/safeMergeObjects.ts create mode 100644 packages/low-router-preact/src/hooks/useCreateRouter.ts create mode 100644 packages/low-router-preact/src/hooks/useRouter.ts create mode 100644 packages/low-router-preact/src/index.ts create mode 100644 packages/low-router-preact/src/preact-deps.ts create mode 100644 packages/low-router-preact/src/router.d.ts create mode 100644 packages/low-router-preact/src/services/I18n.ts create mode 100644 packages/low-router-preact/tsconfig.json create mode 100644 packages/low-router-preact/tsup.config.ts rename {examples => packages/low-router/examples}/.gitkeep (100%) rename {examples => packages/low-router/examples}/basic-resolve-sync/.gitignore (100%) rename {examples => packages/low-router/examples}/basic-resolve-sync/about.html (100%) rename {examples => packages/low-router/examples}/basic-resolve-sync/index.html (100%) rename {examples => packages/low-router/examples}/basic-resolve-sync/package.json (86%) rename {examples => packages/low-router/examples}/basic-resolve-sync/public/vite.svg (100%) rename {examples => packages/low-router/examples}/basic-resolve-sync/src/index.ts (100%) rename {examples => packages/low-router/examples}/basic-resolve-sync/src/style.css (100%) rename {examples/basic => packages/low-router/examples/basic-resolve-sync}/src/vite-env.d.ts (100%) rename {examples => packages/low-router/examples}/basic-resolve-sync/tsconfig.json (100%) rename {examples => packages/low-router/examples}/basic-resolve-sync/vite.config.ts (100%) rename {examples => packages/low-router/examples}/basic/.gitignore (100%) rename {examples => packages/low-router/examples}/basic/about.html (100%) rename {examples => packages/low-router/examples}/basic/index.html (100%) rename {examples => packages/low-router/examples}/basic/package.json (85%) rename {examples => packages/low-router/examples}/basic/public/vite.svg (100%) rename {examples => packages/low-router/examples}/basic/src/index.ts (100%) rename {examples => packages/low-router/examples}/basic/src/style.css (100%) rename {examples/compose => packages/low-router/examples/basic}/src/vite-env.d.ts (100%) rename {examples => packages/low-router/examples}/basic/tsconfig.json (100%) rename {examples => packages/low-router/examples}/basic/vite.config.ts (100%) rename {examples => packages/low-router/examples}/compose/.gitignore (100%) rename {examples => packages/low-router/examples}/compose/about.html (100%) rename {examples => packages/low-router/examples}/compose/contact.html (100%) rename {examples => packages/low-router/examples}/compose/index.html (100%) rename {examples => packages/low-router/examples}/compose/package.json (75%) rename {examples => packages/low-router/examples}/compose/public/vite.svg (100%) rename {examples => packages/low-router/examples}/compose/src/components/App.ts (100%) rename {examples => packages/low-router/examples}/compose/src/components/Button.ts (100%) rename {examples => packages/low-router/examples}/compose/src/compose/Component.ts (100%) rename {examples => packages/low-router/examples}/compose/src/helpers/defaultTransition.ts (100%) rename {examples => packages/low-router/examples}/compose/src/helpers/getTranslate3DValues.ts (100%) rename {examples => packages/low-router/examples}/compose/src/helpers/setRandomBackgroundColor.ts (100%) rename {examples => packages/low-router/examples}/compose/src/index.css (100%) rename {examples => packages/low-router/examples}/compose/src/index.ts (100%) rename {examples => packages/low-router/examples}/compose/src/pages /About.ts (100%) rename {examples => packages/low-router/examples}/compose/src/pages /AboutFoo.ts (100%) rename {examples => packages/low-router/examples}/compose/src/pages /Contact.ts (100%) rename {examples => packages/low-router/examples}/compose/src/pages /Home.ts (100%) rename {examples/custom-path-to-regexp => packages/low-router/examples/compose}/src/vite-env.d.ts (100%) rename {examples => packages/low-router/examples}/compose/tsconfig.json (100%) rename {examples => packages/low-router/examples}/compose/vite.config.ts (100%) rename {examples => packages/low-router/examples}/custom-path-to-regexp/.gitignore (100%) rename {examples => packages/low-router/examples}/custom-path-to-regexp/about.html (100%) rename {examples => packages/low-router/examples}/custom-path-to-regexp/index.html (100%) rename {examples => packages/low-router/examples}/custom-path-to-regexp/package.json (79%) rename {examples => packages/low-router/examples}/custom-path-to-regexp/public/vite.svg (100%) rename {examples => packages/low-router/examples}/custom-path-to-regexp/src/index.ts (100%) rename {examples => packages/low-router/examples}/custom-path-to-regexp/src/style.css (100%) rename {examples/react-nested-routes => packages/low-router/examples/custom-path-to-regexp}/src/vite-env.d.ts (100%) rename {examples => packages/low-router/examples}/custom-path-to-regexp/tsconfig.json (100%) rename {examples => packages/low-router/examples}/custom-path-to-regexp/vite.config.ts (100%) rename {examples => packages/low-router/examples}/react-nested-routes/.gitignore (100%) rename {examples => packages/low-router/examples}/react-nested-routes/README.md (100%) rename {examples => packages/low-router/examples}/react-nested-routes/index.html (100%) rename {examples => packages/low-router/examples}/react-nested-routes/package.json (55%) rename {examples => packages/low-router/examples}/react-nested-routes/public/vite.svg (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/assets/react.svg (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/beeper.ts (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/components/app/App.css (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/components/app/App.tsx (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/index.css (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/index.tsx (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/lowRouterReact/BrowserHistory.tsx (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/lowRouterReact/Router.tsx (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/lowRouterReact/Stack.tsx (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/lowRouterReact/useRouter.tsx (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/pages/A.tsx (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/pages/About.tsx (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/pages/Foo.tsx (100%) rename {examples => packages/low-router/examples}/react-nested-routes/src/routes.tsx (100%) create mode 100644 packages/low-router/examples/react-nested-routes/src/vite-env.d.ts rename {examples => packages/low-router/examples}/react-nested-routes/tsconfig.json (100%) rename {examples => packages/low-router/examples}/react-nested-routes/tsconfig.node.json (100%) rename {examples => packages/low-router/examples}/react-nested-routes/vite.config.ts (100%) diff --git a/.changeset/config.json b/.changeset/config.json index 687be01..a9ac7ea 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,9 +1,9 @@ { - "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", + "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], - "linked": [["@wbe/low-router"]], + "linked": [], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", diff --git a/.changeset/tough-socks-marry.md b/.changeset/tough-socks-marry.md new file mode 100644 index 0000000..5c4dce2 --- /dev/null +++ b/.changeset/tough-socks-marry.md @@ -0,0 +1,90 @@ +--- +"@wbe/low-router-preact": minor +--- + +Low router preact wrapper first release. + +## Usage example + +(check the readme for more information) + +main.tsx: + +```tsx +import { render } from "preact" +import { createBrowserHistory } from "@wbe/wbe-router" +import { Router } from "@wbe/low-router-preact" + +// Prepare the routes list +const routes = [ + { + name: "home", + path: "/", + action: () => Home, + }, + { + name: "about", + path: "/about", + action: () => About, + }, +] + +// Pass the routes list as LowRouter param +const router = new LowRouter(routes) + +// Prepare a browser history +const history = createBrowserHistory() + +// Render the app wrapped by the Router +render( + + + , + document.getElementById("root"), +) +``` + +App.tsx + +```tsx +import { Link, Stack } from "@wbe/low-router-preact" + +export default function App() { + return ( +
+ + {/* Render current route here */} + +
+ ) +} +``` + +Home.tsx + +```tsx +import { useRef, useImperativeHandle } from "preact/hooks" + +const Home = (props, ref) => { + const rootRef = useRef(null) + + // Each route need to attached name, DOM root, playIn & playOut to the forwarded ref. + // The Stack use this forwarded ref in order to control the component. + useImperativeHandle( + ref, + () => ({ + name: "Home", + root: rootRef.current, + playIn: () => customPlayIn(rootRef.current), + playOut: () => customPlayOut(rootRef.current), + }), + [], + ) + return
Hello home!
+} + +export default forwardRef(Home) +``` diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 2f66aba..d2a3210 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,12 +1,13 @@ { "buildCommand": "build", - "packages": ["./packages/*"], + "packages": ["./packages/low-router", "./packages/low-router-preact"], "sandboxes": [ - "./examples/basic", - "./examples/basic-resolve-sync", - "./examples/compose", - "./examples/custom-path-to-regexp", - "./examples/react-nested-routes" + "/packages/low-router/examples/basic", + "/packages/low-router/examples/basic-resolve-sync", + "/packages/low-router/examples/compose", + "/packages/low-router/examples/custom-path-to-regexp", + "/packages/low-router/examples/react-nested-routes", + "/packages/low-router-preact/examples/example-preact-ssr" ], "node": "18" } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c9601f..ae42ced 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,11 +39,11 @@ jobs: run_install: | args: [--filter "@wbe/*"] - - name: Copy README file to package - uses: canastro/copy-file-action@master - with: - source: "README.md" - target: "packages/low-router/README.md" +# - name: Copy README file to package +# uses: canastro/copy-file-action@master +# with: +# source: "README.md" +# target: "packages/low-router/README.md" - name: Set up NPM credentials run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc diff --git a/README.md b/README.md index a0df493..392d0aa 100644 --- a/README.md +++ b/README.md @@ -8,382 +8,11 @@ `LowRouter` is a lightweight _(~=1.8Kb)_, low-level router implementation designed for use in nodejs, javascript or typescript applications. By default, `LowRouter` has no link with the browser history, but this repository provide a `createBrowserHistory` util ready to use. It also includes a `createMatcher` function to convert a route path to a regular expression, but still open to use a custom one. -## Table of Contents +This package contains: -- [Playground](#playground) -- [Installation](#installation) -- [Usage](#usage) - - [instance](#instance) - - [resolve](#resolve) - - [resolveSync](#resolvesync) - - [createUrl](#createurl) - - [dispose](#dispose) -- [Handle history](#handle-history) -- [Matcher](#matcher) -- [Custom matcher](#custom-matcher) -- [debug](#debug) -- [API](#api) - - [LowRouter](#lowrouter) - - [options](#options) - - [Route](#route) - - [RouteContext](#routecontext) - - [createBrowserHistory](#createbrowserhistory) -- [workflow](#workflow) -- [Acknowledgement](#acknowledgement) -- [Credits](#credits) +- [@wbe/low-router](https://github.com/willybrauner/low-router/tree/main/packages/low-router) - low-router core +- [@wbe/low-router-preact](https://github.com/willybrauner/low-router/tree/main/packages/low-router) - high level low-router wrapper for preact -## Playground - -The examples of this repo are available on codesandbox: - -- [basic](https://codesandbox.io/s/github/willybrauner/low-router/tree/main/examples/basic) -- [basic-resolve-sync](https://codesandbox.io/s/github/willybrauner/low-router/tree/main/examples/basic-resolve-sync) -- [compose](https://codesandbox.io/s/github/willybrauner/low-router/tree/main/examples/compose) -- [custom-matcher](https://codesandbox.io/s/github/willybrauner/low-router/tree/main/examples/custom-path-to-regexp) -- [react-nested-routes](https://codesandbox.io/s/github/willybrauner/low-router/tree/main/examples/react-nested-routes) - -## Installation - -```shell -npm i @wbe/low-router -``` - -## Usage - -### Instance - -```javascript -import { LowRouter } from "@wbe/low-router" - -const routes = [ - { - path: "/", - name: "home", - action: () => "Hello home!", - }, - { - path: "/admin", - name: "admin", - action: () => "Hello admin!", - children: [ - { - path: "/config", - name: "config", - action: (context) => `Hello ${context.route.name}!`, - }, - { - path: "/user/:id", - name: "user", - action: (context) => `Hello user! with id ${context.params.id}`, - }, - ], - }, -] - -const router = new LowRouter(routes) -``` - -### resolve - -The `resolve` method allows you to match a given pathname or route object to a defined route and execute its associated action. It returns a Promise that resolves with the action result and route context. - -```js -router.resolve("/").then(({ response, context }) => { - // response: "Hello home!" -}) -``` - -Or, with an object param: - -```js -router.resolve({ name: "user", params: { id: 123 } }).then(({ response, context }) => { - // response: "Hello user! with id 123" -}) -``` - -### resolveSync - -The `resolveSync` method is the same than resolve, but synchronously. It returns the action result and route context directly. - -```js -const { response, context } = router.resolveSync("/admin/config") -// response: "Hello home!" -``` - -Or, with an object param: - -```js -const { response, context } = router.resolveSync({ name: "user", params: { id: 123 } }) -// response: "Hello user! with id 123" -``` - -### createUrl - -The `createUrl` method generates a URL based on a route name and optional parameters. - -```js -router.createUrl({ name: "config" }) -// "/admin/config" -``` - -### dispose - -The `dispose` method is used to clean up the router instance. - -```js -router.dispose() -``` - -## Handle history - -Internal `createBrowserHistory` provide a way to interact with the browser's history and listen to changes in the URL. You can integrate this functionality with the `LowRouter` class to enable client-side routing with browser history support. - -```javascript -import { LowRouter, createBrowserHistory } from "@wbe/low-router" - -const router = new LowRouter(routes, options) -const history = createBrowserHistory() - -const unlisten = history.listen(async ({ location, action }) => { - const response = await router.resolve(location.pathname) - // Do something with the response... -}) - -// Push to the browser history will trigger the router resolve method -history.push("/foo") -history.push(router.createUrl({ name: "bar", params: { id: 123 } })) - -// Stop listening to history changes -unlisten() -``` - -On the same way, you can use every history lib you want to handle history changes, and resolve -the new pathname with the router, like [remix-run/history](https://github.com/remix-run/history). - -## Matcher - -The `matcher` is the function used to convert a route path to a regular expression. By default, `LowRouter` use an [internal matcher function](packages/low-router/src/utils/createMatcher.ts). this matcher is called when the resolve method is called. You shouldn't have to use this function directly, but it's interesting to understand how it works, specially if you need to use a custom one. - -```ts -import { createMatcher } from "@wbe/low-router" - -const matcher = createMatcher() -const [isMatch, routeParams, queryParams, hash] = matcher( - "/user/1?lang=fr&cat=foo#section-2", - "/user/:id" -) -// isMatch: true -// routeParams: { id: "1" } -// queryParams: { lang: "fr", cat: "foo" } -// hash: "section-2" -``` - -This returns values are returned by `RouteContext` when the route match. For more information about the matcher full matcher API, read the [createMatcher unit tests](packages/low-router/tests/createMatcher.test.ts). - -## Custom matcher - -If the internal matcher doesn't respond as needed, it's possible to use a custom matcher function: -like the original [path-to-regexp package](https://github.com/pillarjs/path-to-regexp). - -```ts -import { LowRouter, createMatcher } from "@wbe/low-router" -import { pathToRegexp } from "path-to-regexp" - -const customPathToRegexpFn = (path: string): { keys: Record[]; regexp: RegExp } => { - let keys = [] - const regexp = pathToRegexp(path, keys) - return { keys, regexp } -} - -const customMatcher = createMatcher(customPathToRegexpFn) -// ex: customMatcher("/about/:id", "/about/1") -// return: [true, { id: "1" }, {}, null] - -// then, pass this customMatcher to the router options -// Now, the router will use this custom matcher with path-to-regexp to match routes -const router = new LowRouter(routes, { matcher: customMatcher }) -``` - -This flexible custom matcher pattern as been created by [molefrog](https://github.com/molefrog) on [wouter](https://github.com/molefrog/wouter) 🙏 - -### debug - -[@wbe/debug](https://github.com/willybrauner/debug) tool is used as dependency in this project. To enable debug logs, you can use the following commands: - -- Browser debug: - - ```shell - localStorage.debug = "low-router:*" - ``` - -- Node debug: - - ```shell - DEBUG=low-router:* - ``` - -## API - -### LowRouter - -```ts -// LowRouter(routes: Route[], options?: Options) -const router = new LowRouter(routes, options) - -// Resolve a pathname or a route object -// resolve(pathnameOrObject: string | { name: string; params?: RouteParams }) -router.resolve(path) -router.resolve({ name: "", params: {} }) - -// Resolve synchronously -// resolveSync(pathnameOrObject: string | { name: string; params?: RouteParams }) -router.resolveSync(path) -router.resolveSync({ name: "", params: {} }) - -// Create a URL based on a route name and optional parameters -// createUrl({ name: string; params?: RouteParams }): string -router.createUrl({ name: "", params: {} }) - -// Clean up the router instance -// dispose(): void -router.dispose() -``` - -### Options - -```ts -const options: Options = { - // The base URL path for all routes. - // default: `/`. - base: "/", - - // called when the router is initialized - // onInit: () => void - onInit: () => {}, - - // called when no matching route is found during resolution - // onError: () => void - onError: (context, error) => {}, - - // called after a route's action has been executed successfully - // onResolve: ({response: ActionResponse, context: RouteContext}) => void - onResolve: ({ response, context }) => {}, - - // called when the router is disposed of using the `dispose` method - // onDispose: () => void - onDispose: () => {}, - - // Custom function to convert a route path to a regular expression. - // Default: the internal `createMatcher()` fn - // matcher: Matcher - matcher: createMatcher(), - - // give an id to the router instance, useful when you have multiple router instances - // and you want to identify them from debug logs - // id?: number | string - id: 1, -} -``` - -### RouteContext - -`RouteContext` is the 1st level route object, passed to the route action function. -It contains all the information about the current context, plus the route object itself. - -```ts -interface RouteContext { - // The current pathname - pathname: string - - // The current path params - // (ex: /:foo/:bar) - params: RouteParams - - // The current query params - // (ex: ?foo=bar&baz=qux) - query: QueryParams - - // The current hash - // (ex: #foo) - hash: Hash - - // the route base URL - base: string - - // → the route object associated to this context - route: Route - - // parent route context, useful when the current is a child route - parent: RouteContext | null -} -``` - -### Route - -`Route` is the route object definition passed to the `LowRouter` constructor, define by the developer. - -```ts -interface Route { - // The route path - // (ex: /foo/:bar) - path: string - - // The route name, useful to get a route by name - name?: string - - // The route action function is the main function of the route - // this function is called when the route is resolved - action?: (context: RouteContext) => Promise | any - - // The route children - children?: Route[] - - // The route props can be any data you want to pass/associate to the route - props?: Record -} -``` - -### createBrowserHistory - -`createBrowserHistory()` will return an object: - -```ts -export interface HistoryAPI { - // associate a callback to the history change event - // return a function to stop listening - listen: (callback: ({ location, action }) => void) => () => void - - // Push a new patname to the history - push: (pathname: string) => void -} -``` - -## Workflow - -```shell -# clone repo -git clone {repo} - -# install all dependencies -pnpm i - -# run build watch -pnpm run build:watch - -# run test watch -pnpm run test:watch - -# run examples dev server -pnpm run dev -``` - -## Acknowledgement - -This project is inspired by the following projects: - -- [universal-router](https://github.com/kriasoft/universal-router/) -- [wouter](https://github.com/molefrog/wouter) -- [history](https://github.com/remix-run/history) ## Credits diff --git a/package.json b/package.json index c628a96..0b7b71d 100644 --- a/package.json +++ b/package.json @@ -14,23 +14,28 @@ "test:watch": "vitest --reporter verbose", "test": "vitest run", "size": "size-limit", + "reset": "rm -rf dist node_modules package-lock.json pnpm-lock.yaml tsconfig.tsbuildinfo .turbo", + "reset:all": "FORCE_COLOR=1 turbo run reset && pnpm reset", + "reinstall": "pnpm reset:all && pnpm install", "pre-publish": "npm run build && npm run test", + "ncu": "find . -name 'node_modules' -prune -o -name 'package.json' -execdir ncu -u ';'", "ci:version": "pnpm changeset version && pnpm --filter \"@wbe/*\" install --lockfile-only", "ci:publish": "pnpm build && pnpm changeset publish" }, "devDependencies": { - "@changesets/cli": "^2.26.1", - "@size-limit/esbuild-why": "^9.0.0", - "@size-limit/preset-small-lib": "^8.2.4", - "@types/node": "^20.1.0", - "jsdom": "^22.1.0", - "prettier": "^2.8.8", - "size-limit": "^8.2.4", - "turbo": "^1.9.9", - "typescript": "^5.0.4", - "vite": "^4.3.5", - "vitest": "^0.31.0" + "@changesets/cli": "^2.27.8", + "@size-limit/esbuild-why": "^11.1.4", + "@size-limit/preset-small-lib": "^11.1.4", + "@types/node": "^22.5.4", + "jsdom": "^25.0.0", + "prettier": "^3.3.3", + "size-limit": "^11.1.4", + "turbo": "^2.1.1", + "typescript": "^5.5.4", + "vite": "^5.4.3", + "vitest": "^2.0.5" }, + "packageManager": "pnpm@9.7.0", "prettier": { "semi": false, "printWidth": 100 @@ -40,6 +45,11 @@ "name": "@wbe/low-router", "path": "packages/low-router/dist/low-router.js", "limit": "2.25 KB" + }, + { + "name": "@wbe/low-router-preact", + "path": "packages/low-router-preact/dist/low-router-preact.js", + "limit": "13 KB" } ] } diff --git a/packages/low-router-preact/README.md b/packages/low-router-preact/README.md new file mode 100644 index 0000000..497acf3 --- /dev/null +++ b/packages/low-router-preact/README.md @@ -0,0 +1,443 @@ +

low-router preact

+

+npm +npm bundle size +build +

+

+ +`Low-router preact` is an High level and opinionated Preact router based on [low-router](https://github.com/willybrauner/low-router). +It supports out of the box, SSR routing, nested router instances, I18n and route transitions manager. + +## Table of Contents + +- [Installation](#installation) +- [Usage](#usage) +- [Nested Routes](#nested-routes) +- [i18n](#i18n) + - [setup](#i18n-setup) + - [API](#i18n-api) +- [SSR](#ssr) +- [Components](#components) + - [Router](#router) + - [Stack](#stack) + - [Link](#link) +- [Hooks](#hooks) + - [useRouter](#userouter) + - [useCreateRouter](#usecreaterouter) +- [Utils](#utils) + - [setLocation](#setlocation) +- [Credits](#credits) + +## Installation + +```shell +npm i @wbe/low-router-preact +``` + +## Usage + +To make it works, we need to: + +- Create a routes list witch render a Route component depending on each path. +- Create a `LowRouter` instance & and a `browserHistory` give it as props to `` component. +- Add `` in children of the Router instance. +- "forwardRef" each route components to make it handle by the Stack. + +main.tsx: + +```tsx +import { render } from "preact" +import { createBrowserHistory } from "@wbe/wbe-router" +import { Router } from "@wbe/low-router-preact" + +// Prepare the routes list +const routes = [ + { + name: "home", + path: "/", + action: () => Home, + }, + { + name: "about", + path: "/about", + action: () => About, + }, +] + +// Pass the routes list as LowRouter param +const router = new LowRouter(routes) + +// Prepare a browser history +const history = createBrowserHistory() + +// Render the app wrapped by the Router +render( + + + , + document.getElementById("root") +) +``` + +App.tsx + +```tsx +import { Link, Stack } from "@wbe/low-router-preact" + +export default function App() { + return ( +

+ + {/* Render current route here */} + +
+ ) +} +``` + +Home.tsx + +```tsx +import { useRef, useImperativeHandle } from "preact/hooks" + +const Home = (props, ref) => { + const rootRef = useRef(null) + + // Each route need to attached name, DOM root, playIn & playOut to the forwarded ref. + // The Stack use this forwarded ref in order to control the component. + useImperativeHandle( + ref, + () => ({ + name: "Home", + root: rootRef.current, + playIn: () => customPlayIn(rootRef.current), + playOut: () => customPlayOut(rootRef.current), + }), + [] + ) + return
Hello home!
+} + +export default forwardRef(Home) +``` + +## Nested routes + +`low-router` support nested routes, `low-router-preact` too! +Example of sub routing setup we will that be given to the LowRouter instance, like in the example above: + +```tsx +// ... +const routes = [ + { + name: "home", + path: "/", + action: () => Home, + }, + { + path: "/admin", + name: "admin", + action: () => Admin, + children: [ + // this route is needed to render the parent component + // without children routes in the nested router stack + // it also possible to return a component on this root children root if needed + { + path: "/", + name: "admin-nested-root-route", + }, + { + path: "/config", + name: "config", + action: () => Config, + }, + { + path: "/user/:id", + name: "user", + action: () => User, + }, + ], + }, +] +``` + +Now we need to create a new `` instance with its own `` component in order to render admin's children routes. + +Admin.tsx + +```tsx +import { useRef, useImperativeHandle } from "preact/hooks" + +const Admin = (props, ref) => { + // ... + + const subRouter = useCreateRouter({ from: "admin" }) + + return ( +
+ Admin! + +
+ config + user 1 + +
+
+
+ ) +} + +export default forwardRef(Admin) +``` + +## i18n + +### i18n setup + +`i18n` for "internationalisation", is a built-in class used to manage different locales with the router. +Basically, it allows to switch from a locale to another and translate a path segment. + +main.tsx + +```tsx +const routes = [ + // ... + { + name: "about", + path: "/about", + // translates path fragment as you want + translations: { en: "/about", fr: "/a-propos" }, + action: () => About, + }, +] + +// Shared app base for I18n and LowRouter +const base = "/" + +// Prepare locales list & create i18n instance +const locales = [{ code: "en" }, { code: "fr" }] +const i18n = new I18n(locales, { base, defaultLocaleInUrl: false }) + +// before giving routes to the router instance, we need to patch them by adding +// :locale params to each first level routes +const patchedRoutes = i18n.addLocaleParamToRoutes(routes) + +// then, create the router instance +const router = new LowRouter(patchedRoutes, { base }) + +render( + + + , + document.getElementById("root") +) +``` + +App.tsx + +```tsx +import { Link, Stack, useRouter } from "@wbe/low-router-preact" + +export default function App() { + // Retrieve i18n instance from useRouter hook + // to update the locale, get the currentLocale etc. + const { i18n } = useRouter() + return ( +
+ + + // ... +
+ ) +} +``` + +### i18n API + +#### constructor + +The `I18n` class constructor takes an array of locales and an optional options object + +```ts +type Locale = { + code: T | string + name?: string + default?: boolean +} + +declare class I18n { + constructor( + locales: Locale[], + options?: Partial<{ + base: string + defaultLocaleInUrl: boolean + staticLocation: string + }> + ) +} +``` + +#### Methods + +```ts +const i18n = new I18n(locales, {}); + +// Add Locale to Routes, Patch all first level routes with ":locale" param +i18n.addLocaleParamToRoutes(locale: string): void + +// change the current locale +i18n.setLocale(locale: string): void + +// Redirect to the default locale +i18n.redirectToDefaultLocale(): void + +// Redirect to the browser locale +i18n.redirectToBrowserLocale(): void +``` + +## SSR + +TODO + +## Components + +### Router + +This is the component orchestrating the routing logic thanks to the LowRouter instance passed as props. + +```tsx + + ... + +``` + +#### props + +- **routes**: `Route[]` - routes list +- **options**: `Partial` - router options +- **children**: `ReactElement` - main app component +- **history**: `HistoryAPI | any` - history instance ("any" because it can receive a custom history like "remix/history") +- **staticLocation**: `string` - static location passed to the main Router component if exist +- **i18n**: `I18n` - i18n instance if exist + +### Stack + +The Stack is a routes render manager. It receives routes information from the parent Router component +and manage the transitions between current and prev route. + +#### props + +- **transitions**: `(T: { prev: RouteRef; current: RouteRef; unmountPrev: () => void }) => Promise` - custom transitions function +- **clampRoutesRender**: `boolean` - clamp Routes render number to two routes, prev & next + +The cool thing is that you can pass a custom transitions function to the Stack component. + +example: + +```tsx + { + if (current?.root) current.root.style.opacity = "0" + prev?.playOut?.().then(unmountPrev) + if (current?.root) current.root.style.opacity = "1" + await current?.playIn?.() + }} +/> +``` + +### Link + +This is a simple link component that allows to navigate to a route by clicking on it. + +```tsx +{"home"} +{"about"} +``` + +#### props + +- **to**: `string | { name: string; params?: RouteParams }` - route name or path +- **onClick**: `() => void` - custom click handler +- **className**: `string` - custom class name +- **children**: `ReactElement` - link content +- **ref**: `MutableRefObject` - forwarded ref + +## Hooks + +### useRouter + +Main router hook, it returns all current instance router information (IRouterContext). +Be careful on this point, some IRouterContext information are global between all Router instances, like the history, i18n. + +```ts +const router = useRouter() +``` + +#### returns + +- **prevContext**: `RouteContext` - previous route context +- **currentContext**: `RouteContext` - current route context +- **router**: `LowRouter` - current router instance +- **base**: `string` - base path +- **routes**: `Route[]` - current router routes +- **options**: `Partial` - current router options +- **history**: `HistoryAPI | any` - history instance ("any" because it can receive a custom history like "remix/history") +- **counter**: `number` - routes counter +- **staticLocation**: `string` - static location passed to the main Router component if exist +- **i18n**: `I18n` - i18n instance if exist + +example: + +```tsx +const App = () => { + const { currentContext } = useRouter() + return
{currentContext.route.name}
+} +``` + +### useCreateRouter + +This hook facilitates the sub LowRouter instance creation, just by giving the route name from where +the new Router need to be instantiated. + +```ts +const router = useCreateRouter({ from: "admin", id: 1 }) +``` + +#### params + +- **from**: `string` - route name +- **id**: `number | string` - router id + +#### returns + +- **router**: `LowRouter` - new router instance + +## Utils + +### setLocation + +This utility function allows to navigate to a route by giving a route name or path. +It is useful when you need to navigate from a function or a custom event. + +```ts +setLocation({ to: "blog", params: { id: "foo" } }) +``` + +#### params + +- **to**: `string | { name: string; params?: RouteParams }` - route name or path + +## Credits + +I originally wrote the main logic of this router on [@cher-ami/router](https://github.com/cher-ami/router). +LowRouter allowed me to separate the low level routing logic, from the "Framework usage wrapper" (react, preact etc). +This library is totally independent of his first cher-ami's version. + +LICENCE MIT + +© [Willy Brauner](https://willybrauner.com) diff --git a/packages/low-router-preact/examples/example-preact-ssr/.env b/packages/low-router-preact/examples/example-preact-ssr/.env new file mode 100644 index 0000000..8bb08a5 --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/.env @@ -0,0 +1 @@ +VITE_APP_BASE=/ diff --git a/packages/low-router-preact/examples/example-preact-ssr/.gitignore b/packages/low-router-preact/examples/example-preact-ssr/.gitignore new file mode 100644 index 0000000..f499c30 --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/.gitignore @@ -0,0 +1,12 @@ +dist +vendor +.env.local +node_modules +.DS_Store +.idea +.vscode +*.local +.cache +vite.config.ts.timestamp* +stats.html +tsconfig.tsbuildinfo diff --git a/packages/low-router-preact/examples/example-preact-ssr/config/config.js b/packages/low-router-preact/examples/example-preact-ssr/config/config.js new file mode 100644 index 0000000..5b867ba --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/config/config.js @@ -0,0 +1,18 @@ +import { resolve } from "path" + +export default { + srcDir: resolve("src"), + + // public assets from this folder will be copied in build folder + publicDir: resolve("src/public"), + + // outDir: where files are built + // If this value is change, add this new path in .gitignore + outDir: resolve("dist"), + outDirSsrScripts: resolve("dist/ssr/scripts"), + outDirSsrServer: resolve("dist/ssr/server"), + outDirSsrClient: resolve("dist/ssr/client"), + outDirSpa: resolve("dist/spa"), + outDirStaticClient: resolve("dist/static/client"), + outDirStaticScripts: resolve("dist/static/scripts"), +} diff --git a/packages/low-router-preact/examples/example-preact-ssr/config/vite-plugins/vite-custom-logger.ts b/packages/low-router-preact/examples/example-preact-ssr/config/vite-plugins/vite-custom-logger.ts new file mode 100644 index 0000000..8c67b3a --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/config/vite-plugins/vite-custom-logger.ts @@ -0,0 +1,39 @@ +import { createLogger, Logger } from "vite" +import chalk from "chalk" +import packageJson from "../../package.json" + +export const viteMaestroCustomLogger = ({ + protocol, + host, + port, + base, +}: { + protocol: "http" | "https" + host: string + port: string + base: string +}): Logger => { + const logger = createLogger("info", { allowClearScreen: true }) + const hostIsLocalhost = host === "localhost" + const formatBase = base === "/" ? "" : base + + // prettier-ignore + const template = [ + ``, + ` ${chalk.green.bold.underline(packageJson.name)} ${chalk.gray("v" + packageJson['version'])} \n`, + ` ${chalk.bold("Local")}: ${chalk.cyan(`${protocol}://${chalk.bold('localhost')}${port ? `:${port}`: ""}${formatBase}`)}`, +// !hostIsLocalhost && ` ${chalk.bold("Network")}: ${chalk.cyan(`${protocol}://${chalk.bold(host)}${port ? `:${port}`: ""}${formatBase}`)}`, + ].filter(e => e).join('\n'); + + logger.warnOnce(template) + + const originalInfo = logger.info + logger.info = (msg, options) => { + // return if is default log + if (msg.includes("Local")) return + if (msg.includes("Network")) return + originalInfo(msg, options) + } + + return logger +} diff --git a/packages/low-router-preact/examples/example-preact-ssr/index.html b/packages/low-router-preact/examples/example-preact-ssr/index.html new file mode 100644 index 0000000..5533250 --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/index.html @@ -0,0 +1,16 @@ + + + + + + + SPA APP + + +
+ + + diff --git a/packages/low-router-preact/examples/example-preact-ssr/package.json b/packages/low-router-preact/examples/example-preact-ssr/package.json new file mode 100644 index 0000000..97ee097 --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/package.json @@ -0,0 +1,61 @@ +{ + "name": "example-preact-ssr", + "private": "true", + "main": "src/index-client.tsx", + "type": "module", + "version": "0.1.0", + "scripts": { + "dev": "tsx server.dev.ts", + "build:spa": "cross-env VITE_SPA=true vite build --outDir dist/spa", + "build:ssr-scripts": "vite build -c vite.ssr-scripts.config.ts", + "build:ssr-client": "vite build --outDir dist/ssr/client", + "build:ssr-server": "vite build --ssr src/index-server.tsx --outDir dist/ssr/server", + "build:ssr": "npm run build:ssr-scripts && npm run build:ssr-client && npm run build:ssr-server", + "build": "npm run build:spa && npm run build:ssr", + "start": "cross-env NODE_ENV=production node dist/ssr/scripts/server.prod.js", + "test:watch": "vitest", + "test": "vitest run", + "reset": "rm -rf dist node_modules package-lock.json pnpm-lock.yaml tsconfig.tsbuildinfo", + "reinstall": "pnpm reset && pnpm install" + }, + "dependencies": { + "@wbe/debug": "^1.2.0", + "@wbe/interpol": "^0.15.1", + "@wbe/low-router": "workspace:*", + "@wbe/low-router-preact": "workspace:*", + "@wbe/utils": "^0.1.2", + "fastify": "^4.28.1", + "gsap": "^3.12.5", + "path-to-regexp": "^8.0.0", + "preact": "^10.23.2", + "preact-render-to-string": "^6.5.10" + }, + "devDependencies": { + "@fastify/middie": "^8.3.1", + "@preact/preset-vite": "^2.9.0", + "@types/events": "^3.0.3", + "@types/node": "^22.5.4", + "@vitejs/plugin-legacy": "^5.4.2", + "@wbe/mfs": "^0.3.0", + "autoprefixer": "^10.4.20", + "chalk": "^5.3.0", + "compression": "^1.7.4", + "cross-env": "^7.0.3", + "husky": "^9.1.5", + "portfinder-sync": "^0.0.2", + "prettier": "^3.3.3", + "rollup-plugin-visualizer": "^5.12.0", + "sass": "^1.78.0", + "sirv": "^2.0.4", + "terser": "^5.31.6", + "tsx": "^4.19.0", + "typescript": "^5.5.4", + "vite": "^5.4.3", + "vite-plugin-checker": "^0.8.0", + "vitest": "^2.0.5" + }, + "prettier": { + "semi": false, + "printWidth": 100 + } +} diff --git a/packages/low-router-preact/examples/example-preact-ssr/server.dev.ts b/packages/low-router-preact/examples/example-preact-ssr/server.dev.ts new file mode 100644 index 0000000..a67f0fe --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/server.dev.ts @@ -0,0 +1,74 @@ +import fastify from "fastify" +import fastifyMiddie from "@fastify/middie" +import { createServer, loadEnv } from "vite" +import { renderToStringAsync } from "preact-render-to-string" +import portFinderSync from "portfinder-sync" +import chalk from "chalk" +import config from "./config/config" + +const BASE = "/" +const PORT = portFinderSync.getPort(5173) +const INDEX_SERVER_PATH = `${config.srcDir}/index-server.tsx` +const DEV_SCRIPTS = { + js: [{ tag: "script", attr: { type: "module", src: "/src/index-client.tsx" } }], +} + +async function server() { + const app = fastify() + + const vite = await createServer({ + base: BASE, + appType: "custom", + logLevel: "info", + server: { middlewareMode: true, cors: false }, + }) + + // Handle vite dev-server script HMR & filter requests + await app.register(fastifyMiddie) + app.use(vite.middlewares) + + app.route({ + method: "GET", + url: "*", + onResponse: () => { + // need to be set to make elapsed time available in handler callback + }, + handler: async (req, reply) => { + if (req.url === "/favicon.ico") return + + try { + // Transforms the ESM source code to be usable in Node.js + const { render } = await vite.ssrLoadModule(INDEX_SERVER_PATH) + // Get react-dom from the render method + const dom = await render(req.originalUrl, DEV_SCRIPTS, false, BASE) + // render the string app + const html = await renderToStringAsync(dom) + // send response + reply.status(200) + reply.header("Content-Type", "text/html; charset=utf-8") + reply.send("" + html) + console.log( + chalk.white(`GET ${req.originalUrl}`), + chalk.green(reply.statusCode), + `in ${Math.round(reply.elapsedTime)}ms`, + ) + } catch (e) { + vite.ssrFixStacktrace(e) + reply.log.error(e) + reply.status(500) + console.error(e) + } + }, + }) + + return app +} + +server().then((app) => + app.listen({ port: PORT }, (err) => { + if (err) { + app.log.error(err) + process.exit(1) + } + }), +) diff --git a/packages/low-router-preact/examples/example-preact-ssr/server.prod.ts b/packages/low-router-preact/examples/example-preact-ssr/server.prod.ts new file mode 100644 index 0000000..2d35581 --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/server.prod.ts @@ -0,0 +1,69 @@ +import chalk from "chalk" +import fastify from "fastify" +import fastifyMiddie from "@fastify/middie" +import fs from "node:fs/promises" +import { loadEnv } from "vite" +import portFinderSync from "portfinder-sync" +import { renderToStringAsync } from "preact-render-to-string" +import config from "./config/config" + +const BASE = "/" +const PORT = portFinderSync.getPort(5173) +const MANIFEST_PARSER_PATH = `${config.outDirSsrScripts}/ManifestParser.js` +const VITE_MANIFEST_PATH = `${config.outDirSsrClient}/.vite/manifest.json` +const INDEX_SERVER_PATH = `${config.outDirSsrServer}/index-server.js` + +async function server() { + const app = fastify() + await app.register(fastifyMiddie) + + const compression = (await import("compression")).default + const sirv = (await import("sirv")).default + app.use(compression()) + app.use(BASE, sirv(config.outDirSsrClient, { extensions: [] })) + + app.route({ + method: "GET", + url: "*", + onResponse: () => { + // need to be set to make elapsed time available in handler callback + }, + handler: async (req, reply) => { + try { + // Prepare scripts to inject in template + const { ManifestParser } = await import(MANIFEST_PARSER_PATH) + const manifest = await fs.readFile(VITE_MANIFEST_PATH, "utf-8") + const scriptTags = ManifestParser.getScriptTagFromManifest(manifest, BASE) + + // Prepare & stream the DOM + const { render } = await import(INDEX_SERVER_PATH) + const dom = await render(req.originalUrl, scriptTags, false, BASE) + const html = await renderToStringAsync(dom) + + // send response + reply.status(200) + reply.header("Content-Type", "text/html; charset=utf-8") + reply.send("" + html) + console.log( + chalk.white(`GET ${req.originalUrl}`), + chalk.green(reply.statusCode), + `in ${Math.round(reply.elapsedTime)}ms`, + ) + } catch (e) { + reply.log.error(e) + reply.status(500) + reply.send(e.stack) + } + }, + }) + + return app +} + +server().then((app) => + app.listen({ port: PORT }, () => { + console.log( + `⚡️ ${chalk.white("server.prod is running in")} ${chalk.cyan(`http://localhost:${PORT}`)}`, + ) + }), +) diff --git a/packages/low-router-preact/examples/example-preact-ssr/src/components/App/App.module.scss b/packages/low-router-preact/examples/example-preact-ssr/src/components/App/App.module.scss new file mode 100644 index 0000000..159084d --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/src/components/App/App.module.scss @@ -0,0 +1,6 @@ +.root { + + a { + display: block; + } +} diff --git a/packages/low-router-preact/examples/example-preact-ssr/src/components/App/App.tsx b/packages/low-router-preact/examples/example-preact-ssr/src/components/App/App.tsx new file mode 100644 index 0000000..d2db151 --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/src/components/App/App.tsx @@ -0,0 +1,33 @@ +import css from "./App.module.scss" +import { Link, Stack, StackTransitionsParams, useRouter } from "@wbe/low-router-preact" + +function App() { + const router = useRouter() + + const custom = async ({ prev, current, unmountPrev }: StackTransitionsParams) => { + if (current?.root) current.root.style.opacity = "0" + prev?.playOut?.().then(unmountPrev) + await current?.playIn?.() + } + + return ( +
+
+ + +
+
+ +
+ {"home"} + {"about"} + {"work test-1"} + {"about/bar/bb/dd"} +
+ + +
+ ) +} + +export default App diff --git a/packages/low-router-preact/examples/example-preact-ssr/src/core/server-utils/ManifestParser.ts b/packages/low-router-preact/examples/example-preact-ssr/src/core/server-utils/ManifestParser.ts new file mode 100644 index 0000000..792d135 --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/src/core/server-utils/ManifestParser.ts @@ -0,0 +1,169 @@ +export type TAssetsList = string[] +export type TAssetsByType = { [x: string]: string[] } + +export type TScript = { tag: string; attr: { [x: string]: string } } +export type TScriptsObj = { + [ext: string]: TScript[] +} + +/** + * ManifestParser + * Allow to get scriptTags + * + * + * + */ +export class ManifestParser { + /** + * Directly get script Tags from raw manifest string + * @param manifestRaw + * @param base + */ + static getScriptTagFromManifest(manifestRaw: string, base = "/"): TScriptsObj { + const assets = ManifestParser.getAssets(manifestRaw) + const assetsByType = ManifestParser.sortAssetsByType(assets) + return ManifestParser.getScripts(assetsByType, base) + } + + /** + * Get script tags + * + * ex: + * { + * js: [ + * { + * tag: 'script', + * attr: { + * src: "" + * noModule: "", + * } + * }, + * } + * ... + * + * @param assetListByType + * @param base + */ + static getScripts(assetListByType: TAssetsByType, base = "/"): TScriptsObj { + if (typeof assetListByType !== "object" || !assetListByType) { + console.error("assetListByType is not valid, return", assetListByType) + return + } + // prettier-ignore + return Object.keys(assetListByType).reduce((a, b: string) => + { + const scriptURLs = assetListByType[b] + let scripts: TScript[] + if (b === "js") { + scripts = scriptURLs.map(url => { + return { + tag: "script", + attr: { + ...(url.includes("legacy") ? {noModule: ""} : {type: "module"}), + crossOrigin:"anonymous", + src: `${base}${url}` + } + } + }) + } + else if (b === "css") { + scripts = scriptURLs.map(url => ({ + tag: "link", + attr: { + rel: "stylesheet", + href: `${base}${url}` + } + })) + } + else if (b === "woff2") { + scripts = scriptURLs.map(url => ({ + tag: "link", + attr: { + rel: "preload", + as: "font", + type:"font/woff2", + crossOrigin:"anonymous", + href: `${base}${url}` + } + })) + } + return { ...a, ...(scripts ? {[b]: scripts} : {}) } + },{}) + } + + /** + * + * Get assets by type (by extension): + * ex: + * { + * js: [ + * 'index-legacy-e92b0b23.js', + * 'polyfills-legacy-163e9122.js', + * 'index-475b5da0.js' + * ], + * woff2: [ + * 'roboto-regular-8cef0863.woff2', + * 'roboto-regular-8cef0863.woff2' + * ], + * css: [ + * 'index-ef71c845.css' + * ], + * ... + * } + * + * Group by extensions + * @param assetList + */ + static sortAssetsByType(assetList: TAssetsList): TAssetsByType { + return assetList.reduce((a, b) => { + const ext = b.split(".")[b.split(".").length - 1] + if (a?.[ext] && !a[ext].includes(b)) { + a[ext].push(b) + return a + } else { + return { + ...a, + [ext]: [b], + } + } + }, {}) + } + + /** + * Get assets list + * + * [ + * 'index-legacy-e92b0b23.js', + * 'roboto-regular-8cef0863.woff2', + * 'roboto-regular-18ab5ae4.woff', + * 'roboto-regular-b122d9b1.ttf', + * 'polyfills-legacy-163e9122.js', + * 'index-475b5da0.js', + * ] + * + * + * Return all assets + * @param manifestRawFile + */ + static getAssets(manifestRawFile: string): TAssetsList { + if (!manifestRawFile) return + const jsonManifest = JSON.parse(manifestRawFile) + + const list = Object.keys(jsonManifest) + .reduce( + (a, b) => + jsonManifest[b].isEntry + ? [ + ...a, + jsonManifest[b].file, + ...(jsonManifest[b]?.assets || []), + ...(jsonManifest[b]?.css || []), + ] + : a, + [], + ) + .filter((e) => e) + + return [...new Set(list)] + } +} diff --git a/packages/low-router-preact/examples/example-preact-ssr/src/core/server-utils/RawScript.tsx b/packages/low-router-preact/examples/example-preact-ssr/src/core/server-utils/RawScript.tsx new file mode 100644 index 0000000..80393a9 --- /dev/null +++ b/packages/low-router-preact/examples/example-preact-ssr/src/core/server-utils/RawScript.tsx @@ -0,0 +1,16 @@ +/** + * Insert raw script in window variable + * @param name + * @param obj + */ +export const RawScript = ({ name, data }) => { + const stringify = (e): string => JSON.stringify(e, null, 2)?.replace(/\n\s+/g, "") + return ( +