Repository: preactjs/preact-iso Branch: main Commit: f554b913f5a0 Files: 48 Total size: 200.4 KB Directory structure: gitextract_qae3wprk/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── jsconfig.json ├── package.json ├── polyglot-utils/ │ ├── .gitignore │ ├── README.md │ ├── go/ │ │ ├── README.md │ │ ├── go.mod │ │ ├── preact_iso_url_pattern.go │ │ └── preact_iso_url_pattern_test.go │ ├── php/ │ │ ├── README.md │ │ ├── preact-iso-url-pattern.php │ │ └── test_preact_iso_url_pattern.php │ ├── python/ │ │ ├── README.md │ │ ├── preact_iso_url_pattern.py │ │ └── test_preact_iso_url_pattern.py │ ├── ruby/ │ │ ├── README.md │ │ ├── preact-iso-url-pattern.rb │ │ └── test_preact_iso_url_pattern.rb │ └── run_tests.sh ├── src/ │ ├── hydrate.d.ts │ ├── hydrate.js │ ├── index.d.ts │ ├── index.js │ ├── internal.d.ts │ ├── lazy.d.ts │ ├── lazy.js │ ├── prerender.d.ts │ ├── prerender.js │ ├── router-navigation-api.d.ts │ ├── router-navigation-api.js │ ├── router.d.ts │ └── router.js ├── test/ │ ├── lazy.test.js │ ├── node/ │ │ ├── location-stub.test.js │ │ ├── pattern-match.types.ts │ │ ├── prerender.test.js │ │ └── router-match.test.js │ ├── router-navigation-api.test.js │ ├── router.test.js │ └── setup.js └── web-test-runner.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [{.*rc,*.yml}] indent_style = space indent_size = 2 [package.json] insert_final_newline = false [*.md] trim_trailing_whitespace = false [test/fixtures/**/*.expected.*] trim_trailing_whitespace = false insert_final_newline = false ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- - [ ] Check if updating to the latest `preact-iso` version resolves the issue **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Please provide a link to a StackBlitz/CodeSandbox/Codepen project or a GitHub repository that demonstrates the issue. You can use the following template on StackBlitz to get started: https://stackblitz.com/edit/create-preact-starter-routing Issues without reproductions will likely be closed, they're essential for ensuring we're all on the same page. Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. See error **Expected behavior** What should have happened when following the steps above? ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: feature request assignees: '' --- **Describe the feature you'd love to see** A clear and concise description of what you'd love to see added to `preact-iso`. **Additional context (optional)** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main pull_request: branches: - '**' jobs: build_test: name: Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - name: Install dependencies run: npm ci - name: Tests run: npm run test ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/* !.yarn/releases !.yarn/plugins .pnp.* # testing /coverage # production /dist /build /.yalc # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@preactjs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 The Preact Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # preact-iso [![Preact Slack Community](https://img.shields.io/badge/slack-Preact%20Slack%20Community-blue?logo=slack)](https://chat.preactjs.com/) Isomorphic async tools for Preact. - Lazy-load components using `lazy()` and ``, which also enables progressive hydration. - Generate static HTML for your app using `prerender()`, waiting for `lazy()` components and data dependencies. - Implement async-aware client and server-side routing using ``, including seamless async transitions. --- - [Routing](#routing) - [Prerendering](#prerendering) - [Nested Routing](#nested-routing) - [Non-JS Servers](#non-js-servers) - [API Docs](#api-docs) - [\](#locationprovider) - [\](#router) - [\](#route) - [Path Segment Matching](#path-segment-matching) - [useLocation()](#uselocation) - [useRoute()](#useroute) - [lazy()](#lazy) - [\](#errorboundary) - [hydrate()](#hydrate) - [prerender()](#prerender) - [locationStub()](#locationstub) - [Navigation API Entry Docs](#navigation-api-entry-docs) - [Differences in usage](#differences-in-usage) --- ## Routing `preact-iso` offers a simple router for Preact with conventional and hooks-based APIs. The `` component is async-aware: when transitioning from one route to another, if the incoming route suspends (throws a Promise), the outgoing route is preserved until the new one becomes ready. ```js import { lazy, LocationProvider, ErrorBoundary, Router, Route } from 'preact-iso'; // Synchronous import Home from './routes/home.js'; // Asynchronous (throws a promise) const Profiles = lazy(() => import('./routes/profiles.js')); const Profile = lazy(() => import('./routes/profile.js')); const NotFound = lazy(() => import('./routes/_404.js')); const App = () => ( {/* Alternative dedicated route component for better TS support */} {/* `default` prop indicates a fallback route. Useful for 404 pages */} ); ``` **Progressive Hydration:** When the app is hydrated on the client, the route (`Home` or `Profile` in this case) suspends. This causes hydration for that part of the page to be deferred until the route's `import()` is resolved, at which point that part of the page automatically finishes hydrating. **Seamless Routing:** When switching between routes on the client, the Router is aware of asynchronous dependencies in routes. Instead of clearing the current route and showing a loading spinner while waiting for the next route, the router preserves the current route in-place until the incoming route has finished loading, then they are swapped. ## Prerendering `prerender()` renders a Virtual DOM tree to an HTML string using [`preact-render-to-string`](https://github.com/preactjs/preact-render-to-string). The Promise returned from `prerender()` resolves to an Object with `html` and `links[]` properties. The `html` property contains your pre-rendered static HTML markup, and `links` is an Array of any non-external URL strings found in links on the generated page. Primarily meant for use with prerendering via [`@preact/preset-vite`](https://github.com/preactjs/preset-vite#prerendering-configuration) or other prerendering systems that share the API. If you're server-side rendering your app via any other method, you can use `preact-render-to-string` (specifically `renderToStringAsync()`) directly. ```js import { LocationProvider, ErrorBoundary, Router, lazy, prerender as ssr } from 'preact-iso'; // Asynchronous (throws a promise) const Foo = lazy(() => import('./foo.js')); const App = () => ( ); hydrate(); export async function prerender(data) { return await ssr(); } ``` ## Nested Routing Some applications would benefit from having routers of multiple levels, allowing to break down the routing logic into smaller components. This is especially useful for larger applications, and we solve this by allowing for multiple nested `` components. Partially matched routes end with a wildcard (`/*`) and only the remaining value will be passed to descendant routers for further matching. This allows you to create a parent route that matches a base path, and then have child routes that match specific sub-paths. ```js import { lazy, LocationProvider, ErrorBoundary, Router, Route } from 'preact-iso'; import AllMovies from './routes/movies/all.js'; const NotFound = lazy(() => import('./routes/_404.js')); const App = () => ( ); const TrendingMovies = lazy(() => import('./routes/movies/trending.js')); const SearchMovies = lazy(() => import('./routes/movies/search.js')); const MovieDetails = lazy(() => import('./routes/movies/details.js')); const Movies = () => ( ); ``` The `` component will be used for the following routes: - `/movies/trending` - `/movies/search` - `/movies/Inception` - `/movies/...` It will not be used for any of the following: - `/movies` - `/movies/` ## Non-JS Servers For those using non-JS servers (e.g., PHP, Python, Ruby, etc.) to serve your Preact app, you may want to use our ["polyglot-utils"](./polyglot-utils), a collection of our route matching logic ported to various other languages. Combined with a route manifest, this will allow your server to better understand which assets will be needed at runtime for a given URL, allowing you to say insert preload tags for those assets in the HTML head prior to serving the page. --- ## API Docs ### `LocationProvider` A context provider that provides the current location to its children. This is required for the router to function. Props: - `scope?: string | RegExp` - Sets a scope for the paths that the router will handle (intercept). If a path does not match the scope, either by starting with the provided string or matching the RegExp, the router will ignore it and default browser navigation will apply. Typically, you would wrap your entire app in this provider: ```js import { LocationProvider } from 'preact-iso'; const App = () => ( {/* Your app here */} ); ``` #### Restore default browser navigation for one link The location provider intercepts interactions upon anchor tags (``) to facilitate single-page app navigation. Some interactions, however, are ignored to avoid hindering users as well as giving devs ways to opt-out and resume standard browser behavior. The rules for ignoring are as such: 1. You hold a modifier key (ctrl, meta, alt, shift) or click a mouse button other than the main one. 2. Cross-origin links, e.g., you're on `https://example.org` but the link points to `https://example.com`. 3. The link points to a fragment identifier on the current page, e.g., to `#section`. 4. The link's `target` is set to anything but `_self` (such as `_blank` or `_top`) -- this is a convenient way to opt-out. 5. The link points outside the `scope` of your `LocationProvider`. 6. The link has a `download` attribute. ### `Router` Props: - `onRouteChange?: (url: string) => void` - Callback to be called when a route changes. - `onLoadStart?: (url: string) => void` - Callback to be called when a route starts loading (i.e., if it suspends). This will not be called before navigations to sync routes or subsequent navigations to async routes. - `onLoadEnd?: (url: string) => void` - Callback to be called after a route finishes loading (i.e., if it suspends). This will not be called after navigations to sync routes or subsequent navigations to async routes. ```js import { LocationProvider, Router } from 'preact-iso'; const App = () => ( console.log('Route changed to', url)} onLoadStart={(url) => console.log('Starting to load', url)} onLoadEnd={(url) => console.log('Finished loading', url)} > ); ``` ### `Route` There are two ways to define routes using `preact-iso`: 1. Append router params to the route components directly: `` 2. Use the `Route` component instead: `` Appending arbitrary props to components not unreasonable in JavaScript, as JS is a dynamic language that's perfectly happy to support dynamic & arbitrary interfaces. However, TypeScript, which many of us use even when writing JS (via TS's language server), is not exactly a fan of this sort of interface design. TS does not (yet) allow for overriding a child's props from the parent component so we cannot, for instance, define `` as taking no props _unless_ it's a child of a ``, in which case it can have a `path` prop. This leaves us with a bit of a dilemma: either we define all of our routes as taking `path` props so we don't see TS errors when writing `` or we create wrapper components to handle the route definitions. While `` is completely equivalent to ``, TS users may find the latter preferable. ```js import { LocationProvider, Router, Route } from 'preact-iso'; const App = () => ( {/* Both of these are equivalent */} ); ``` Props for any route component: - `path: string` - The path to match (read on) - `default?: boolean` - If set, this route is a fallback/default route to be used when nothing else matches Specific to the `Route` component: - `component: AnyComponent` - The component to render when the route matches #### Path Segment Matching Paths are matched using a simple string matching algorithm. The following features may be used: - `:param` - Matches any URL segment, binding the value to the label (can later extract this value from `useRoute()`) - `/profile/:id` will match `/profile/123` and `/profile/abc` - `/profile/:id?` will match `/profile` and `/profile/123` - `/profile/:id*` will match `/profile`, `/profile/123`, and `/profile/123/abc` - `/profile/:id+` will match `/profile/123`, `/profile/123/abc` - `*` - Matches one or more URL segments - `/profile/*` will match `/profile/123`, `/profile/123/abc`, etc. These can then be composed to create more complex routes: - `/profile/:id/*` will match `/profile/123/abc`, `/profile/123/abc/def`, etc. The difference between `/:id*` and `/:id/*` is that in the former, the `id` param will include the entire path after it, while in the latter, the `id` is just the single path segment. - `/profile/:id*`, with `/profile/123/abc` - `id` is `123/abc` - `/profile/:id/*`, with `/profile/123/abc` - `id` is `123` You can narrow prop types for your routes using `RoutePropsForPath`: ```ts import type { RoutePropsForPath } from 'preact-iso' function User(props: RoutePropsForPath<'/user/:id'>) { props.user.id2 // type error props.user.id // no type error } ``` ### `useLocation` A hook to work with the `LocationProvider` to access location context. Returns an object with the following properties: - `url: string` - The current path & search params - `path: string` - The current path - `query: Record` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`) - `route: (url: string, replace?: boolean) => void` - A function to programmatically navigate to a new route. The `replace` param can optionally be used to overwrite history, navigating them away without keeping the current location in the history stack. - `back: () => void` - A function to programmatically navigate back one entry in the browser history stack. - `forward: () => void` - A function to programmatically navigate forward one entry in the browser history stack. ### `useRoute` A hook to access current route information. Unlike `useLocation`, this hook only works within `` components. Returns an object with the following properties: - `path: string` - The current path - `query: Record` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`) - `params: Record` - The current route parameters (`/profile/:id` -> `{ id: '123' }`) ### `lazy` Make a lazily-loaded version of a Component. `lazy()` takes an async function that resolves to a Component, and returns a wrapper version of that Component. The wrapper component can be rendered right away, even though the component is only loaded the first time it is rendered. ```js import { lazy, LocationProvider, Router } from 'preact-iso'; // Synchronous, not code-splitted: import Home from './routes/home.js'; // Asynchronous, code-splitted: const Profiles = lazy(() => import('./routes/profiles.js').then(m => m.Profiles)); // Expects a named export called `Profiles` const Profile = lazy(() => import('./routes/profile.js')); // Expects a default export const App = () => ( ); ``` The result of `lazy()` also exposes a `preload()` method that can be used to load the component before it's needed for rendering. Entirely optional, but can be useful on focus, mouse over, etc. to start loading the component a bit earlier than it otherwise would be. ```js const Profile = lazy(() => import('./routes/profile.js')); function Home() { return ( Profile.preload()}> Profile Page -- Hover over me to preload the module! ); } ``` ### `ErrorBoundary` A simple component to catch errors in the component tree below it. Props: - `onError?: (error: Error) => void` - A callback to be called when an error is caught ```js import { LocationProvider, ErrorBoundary, Router } from 'preact-iso'; const App = () => ( console.log(e)}> ); ``` ### `hydrate` A thin wrapper around Preact's `hydrate` export, it switches between hydrating and rendering the provided element, depending on whether the current page has been prerendered. Additionally, it checks to ensure it's running in a browser context before attempting any rendering, making it a no-op during SSR. Pairs with the `prerender()` function. Params: - `jsx: ComponentChild` - The JSX element or component to render - `parent?: Element | Document | ShadowRoot | DocumentFragment` - The parent element to render into. Defaults to `document.body` if not provided. ```js import { hydrate } from 'preact-iso'; const App = () => (

Hello World

); hydrate(); ``` However, it is just a simple utility method. By no means is it essential to use, you can always use Preact's `hydrate` export directly. ### `prerender` Renders a Virtual DOM tree to an HTML string using `preact-render-to-string`. The Promise returned from `prerender()` resolves to an Object with `html` and `links[]` properties. The `html` property contains your pre-rendered static HTML markup, and `links` is an Array of any non-external (either no `target` or something other than `target="_self"`) URL strings found in links on the generated page. Pairs primarily with [`@preact/preset-vite`](https://github.com/preactjs/preset-vite#prerendering-configuration)'s prerendering. Params: - `jsx: ComponentChild` - The JSX element or component to render ```js import { LocationProvider, ErrorBoundary, Router, lazy, prerender } from 'preact-iso'; // Asynchronous (throws a promise) const Foo = lazy(() => import('./foo.js')); const Bar = lazy(() => import('./bar.js')); const App = () => ( ); const { html, links } = await prerender(); ``` ### `locationStub` A utility function to imitate the `location` object in a non-browser environment. Our router relies upon this to function, so if you are using `preact-iso` outside of a browser context and are not prerendering via `@preact/preset-vite` (which does this for you), you can use this utility to set a stubbed `location` object. ```js import { locationStub } from 'preact-iso/prerender'; locationStub('/foo/bar?baz=qux#quux'); console.log(location.pathname); // "/foo/bar" ``` ## Navigation API Entry Docs The Navigation API is a new web standard that provides an updated method of handling "navigation" in web applications, supporting SPA-style routing as a first-class citizen. The older History API can be wrangled to support this and has been the standard for many years, but the Navigation API provides a much more robust set of tools that are really, really attractive for routers like `preact-iso` to take advantage of. Whilst the API [sees fairly wide support](https://caniuse.com/wf-navigation), it is still newly available and thus may not be viable for some targets. As such, we've provided a new entry point that will allow you to take advantage of this API if you wish, but the default remains targetting the History API. The Navigation API entry point is available at `preact-iso/router/navigation-api`. ### Differences in usage The differences lie entirely within the [`useLocation()`](#uselocation) hook: instead of returning a `route()` function, you use the global `navigation` object to perform all navigations. The [`navigation` object](https://developer.mozilla.org/en-US/docs/Web/API/Navigation) contains many of the useful utilities that go along with a router, like `.forward()`, `.back()`, `.canGoForward()`, `.canGoBack()`, `.entries()`, etc. It actually offers far more utilities than the base router did, and does so with less library code overall, so if you have access to it, it's a really nice upgrade. ## License [MIT](./LICENSE) ================================================ FILE: jsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "module": "NodeNext", "moduleResolution": "NodeNext", "noEmit": true, "allowJs": true, "checkJs": true, "skipLibCheck": false, "jsx": "react", "jsxFactory": "h", "jsxFragmentFactory": "Fragment", } } ================================================ FILE: package.json ================================================ { "name": "preact-iso", "version": "2.12.0", "type": "module", "main": "src/index.js", "module": "src/index.js", "types": "src/index.d.ts", "exports": { ".": "./src/index.js", "./router": "./src/router.js", "./router/navigation-api": "./src/router-navigation-api.js", "./lazy": "./src/lazy.js", "./prerender": "./src/prerender.js", "./hydrate": "./src/hydrate.js" }, "sideEffects": false, "license": "MIT", "description": "Isomorphic utilities for Preact", "author": "The Preact Authors (https://preactjs.com)", "repository": { "type": "git", "url": "git+https://github.com/preactjs/preact-iso.git" }, "files": [ "src", "!src/internal.d.ts", "LICENSE", "package.json", "README.md" ], "scripts": { "test": "npm run test:node && npm run test:browser", "test:browser": "wtr test/*.test.js", "test:node": "uvu test/node" }, "peerDependencies": { "preact": ">=10 || >= 11.0.0-0", "preact-render-to-string": ">=6.4.0" }, "devDependencies": { "@types/mocha": "^10.0.7", "@types/sinon-chai": "^3.2.12", "@web/dev-server-esbuild": "^1.0.2", "@web/test-runner": "^0.18.3", "chai": "^5.1.1", "htm": "^3.1.1", "kleur": "^4.1.5", "navigation-api-types": "^0.6.1", "preact": "^10.26.5", "preact-render-to-string": "^6.6.1", "sinon": "^18.0.0", "sinon-chai": "^4.0.0", "uvu": "^0.5.6" }, "overrides": { "@web/dev-server-core": "0.7.1" } } ================================================ FILE: polyglot-utils/.gitignore ================================================ # Language-specific files to ignore __pycache__/ *.pyc .venv/ .ruby-version ================================================ FILE: polyglot-utils/README.md ================================================ # Preact ISO URL Pattern Matching - Polyglot Utils Multi-language implementations of URL pattern matching utilities for building bespoke server setups that need to preload JS/CSS resources or handle early 404 responses. ## Use Case This utility is designed for server languages that **cannot do SSR/prerendering** but still want to provide better experiences. It enables servers to: - **Add preload head tags** for JS,CSS before serving HTML - **Return early 404 pages** for unmatched routes - **Generate dynamic titles** based on route parameters ## How can I implement preloading of JS, CSS? Typical implementation flow: 1. **Build-time Setup:** - Write your routes as an array in a JS file - Create a build script that exports route patterns and entry files to a `.json` file - Configure your frontend build tool to output a `manifest` file mapping entry files to final fingerprinted/hashed output JS/CSS files and dependencies 2. **Server-time Processing:** - Load the JSON route file when a request comes in - Match the requested URL against each route pattern until you find a match - Once matched, you have the source entry `.jsx` file - Load the build manifest file to find which JS chunk contains that code and its dependency files - Generate `` tags for each dependency (JS, CSS, images, icons) - Inject those head tags into the HTML before serving 3. **Result:** - Browsers start downloading critical resources immediately - Faster page loads without full SSR complexity - Early 404s for invalid routes ### Example - preloading of JS, CSS Here's how you might integrate this into a server setup. Let's say you have a client side `routes.js` as follows: ```js import { lazy } from 'preact-iso'; export const routes = [ { "path": "/users/:userId/posts", "component": lazy(() => import("pages/UserPosts.jsx")), "title": "Posts by :userId" }, { "path": "/products/:category/:id", "component": lazy(() => import("pages/Product.jsx")), "title": "Product :id" } ]; ``` 1. **Generate Routes JSON (routes.json)** You can use the following standalone node.js script to create `routes.json` during build (you could convert it into a plugin for your frontend build tool): ```js const routeDir = path.resolve(__dirname, 'client/src/routes'); let routesFile = fs.readFileSync(path.resolve(routeDir, 'routes.js'), 'utf-8'); routesFile = routesFile.replace(/lazy\s*\(\s*\(\s*\)\s*=>\s*import\s*\(\s*(.+)\s*\)\s*\)\s*(,?)/g, '$1$2'); const fileName = path.resolve(__dirname, 'routes-temp.js') fs.writeFileSync(fileName, routesFile, 'utf-8'); const routes = (await import(fileName)).default; fs.unlinkSync(fileName); const routeInfo = routes.map((route) => ({ path: route.path, title: typeof route.title === 'string' ? route.title : null, Component: path.relative(__dirname, path.resolve(routeDir, `${route.Component}.jsx`)), default: route.default, })); // console.log(routeInfo); fs.writeFileSync( path.resolve(__dirname, 'dist/routes.json'), JSON.stringify(routeInfo, null, 2), 'utf-8' ); ``` The script produces the following `routes.json` file: ```json [ { "path": "/users/:userId/posts", "component": "pages/UserPosts.jsx", "title": "Posts by :userId" }, { "path": "/products/:category/:id", "component": "pages/Product.jsx", "title": "Product :id" } ] ``` 2. **Build Manifest (manifest.json)** This is the file your client build tool generates. Check your build tool's documentation for exact format. Below is an example with few important fields from a Vite manifest file: ```json { "pages/UserPosts.jsx": { "file": "assets/UserPosts-abc123.js", "css": ["assets/UserPosts-def456.css"], "imports": ["chunks/shared-ghi789.js"] } } ``` 3. **Server Implementation** ```python # Python example import json routes = json.load(open('../dist/routes.json')) manifest = json.load(open('../dist/.vite/manifest.json')) def handle_request(url_path): for route in routes: matches = preact_iso_url_pattern_match(url_path, route['path']) if matches: # Generate preload tags component = route['component'] entry_info = manifest[component] preload_tags = [] for js_file in [entry_info['file']] + entry_info.get('imports', []): preload_tags.append(f'') for css_file in entry_info.get('css', []): preload_tags.append(f'') # Generate dynamic title title = route['title'] for param, value in matches['params'].items(): title = title.replace(f':{param}', value) return { 'preload_tags': preload_tags, 'title': title, 'params': matches['params'] } # No match found - return early 404 return None ``` This approach gives you the performance benefits of resource preloading without the complexity of full server-side rendering. ## Available Languages Go, PHP, Python and Ruby. Find the corresponding language's sub-directory. Each language has a README that contains usage examples and API reference. ## Running Tests ```bash # Run all tests across all languages ./run_tests.sh # Or run individual language tests cd go && go test -v cd python && python3 test_preact_iso_url_pattern.py cd ruby && ruby test_preact_iso_url_pattern.rb cd php && php test_preact_iso_url_pattern.php ``` ================================================ FILE: polyglot-utils/go/README.md ================================================ # Go Implementation URL pattern matching utility for Go servers. ## Setup Code tested on Go 1.24.x. ```sh # If using in a project, initialize go module go mod init myproject # No third party dependencies needed. Just run the tests or use the function directly ``` ## Running Tests ```sh go test -v ``` ## Usage ```go package main import "fmt" func main() { matches := preactIsoUrlPatternMatch("/users/test%40example.com/posts", "/users/:userId/posts", nil) if matches != nil { fmt.Printf("User ID: %s\n", matches.Params["userId"]) // Output: test@example.com } } ``` ## Function Signature ```go func preactIsoUrlPatternMatch(url, route string, matches *Matches) *Matches ``` ### Parameters - `url` (string): The URL path to match - `route` (string): The route pattern with parameters - `matches` (*Matches): Optional pre-existing matches to extend ### Return Value Returns a `*Matches` struct on success, or `nil` if no match: ```go type Matches struct { Params map[string]string Rest string } ``` ## Route Patterns | Pattern | Description | Example | |---------|-------------|---------| | `/users/:id` | Named parameter | `{id: "123"}` | | `/users/:id?` | Optional parameter | `{id: ""}` | | `/files/:path+` | Required rest parameter | `{path: "docs/readme.txt"}` | | `/static/:path*` | Optional rest parameter | `{path: "css/main.css"}` | | `/static/*` | Anonymous wildcard | `{Rest: "/images/logo.png"}` | ================================================ FILE: polyglot-utils/go/go.mod ================================================ module myproject go 1.13 ================================================ FILE: polyglot-utils/go/preact_iso_url_pattern.go ================================================ // Run program: go run preact-iso-url-pattern.go package main import ( // "fmt" "net/url" "regexp" "strings" ) type Matches struct { Params map[string]string `json:"params"` Rest string `json:"rest,omitempty"` } func preactIsoUrlPatternMatch(urlStr, route string, matches *Matches) *Matches { if matches == nil { matches = &Matches{ Params: make(map[string]string), } } urlParts := filterEmpty(strings.Split(urlStr, "/")) routeParts := filterEmpty(strings.Split(route, "/")) for i := 0; i < max(len(urlParts), len(routeParts)); i++ { var m, param, flag string if i < len(routeParts) { re := regexp.MustCompile(`^(:?)(.*?)([+*?]?)$`) matches := re.FindStringSubmatch(routeParts[i]) if len(matches) > 3 { m, param, flag = matches[1], matches[2], matches[3] } } var val string if i < len(urlParts) { val = urlParts[i] } // segment match: if m == "" && param != "" && param == val { continue } // /foo/* match if m == "" && val != "" && flag == "*" { matches.Rest = "/" + strings.Join(urlParts[i:], "/") break } // segment mismatch / missing required field: if m == "" || (val == "" && flag != "?" && flag != "*") { return nil } rest := flag == "+" || flag == "*" // rest (+/*) match: if rest { decodedParts := make([]string, len(urlParts[i:])) for j, part := range urlParts[i:] { decoded, err := url.QueryUnescape(part) if err != nil { decoded = part // fallback to original if decode fails } decodedParts[j] = decoded } val = strings.Join(decodedParts, "/") } else if val != "" { // normal/optional field: decode val (like JavaScript does) decoded, err := url.QueryUnescape(val) if err != nil { decoded = urlParts[i] } val = decoded } matches.Params[param] = val if rest { break } } return matches } func filterEmpty(s []string) []string { var result []string for _, str := range s { if str != "" { result = append(result, str) } } return result } func max(a, b int) int { if a > b { return a } return b } // Example usage: // func main() { // params := &Matches{Params: make(map[string]string)} // fmt.Println(preactIsoUrlPatternMatch("/foo/bar%20baz", "/foo/:param", params)) // // params := &Matches{Params: make(map[string]string)} // fmt.Println(preactIsoUrlPatternMatch("/foo/bar/baz", "/foo/*")) // // params := &Matches{Params: make(map[string]string)} // fmt.Println(preactIsoUrlPatternMatch("/foo", "/foo/:param?")) // // params := &Matches{Params: make(map[string]string)} // fmt.Println(preactIsoUrlPatternMatch("/foo/bar", "/bar/:param")) // // params := &Matches{Params: make(map[string]string)} // fmt.Println(preactIsoUrlPatternMatch("/users/test%40example.com/posts", "/users/:userId/posts")) // } ================================================ FILE: polyglot-utils/go/preact_iso_url_pattern_test.go ================================================ package main import ( "reflect" "strings" "testing" ) // Note 1: This is different from JS implementation. Here it is empty string and not nil // This was intentionally implemented this way, but we can change if it's a problem func TestPreactIsoUrlPatternMatch(t *testing.T) { tests := []struct { name string url string route string matches *Matches expected *Matches }{ // Base route tests { name: "Base route - exact match", url: "/", route: "/", matches: nil, expected: &Matches{Params: map[string]string{}}, }, { name: "Base route - no match", url: "/user/1", route: "/", matches: nil, expected: nil, }, // Param route tests { name: "Param route - match", url: "/user/2", route: "/user/:id", matches: nil, expected: &Matches{ Params: map[string]string{"id": "2"}, }, }, { name: "Param route - no match", url: "/", route: "/user/:id", matches: nil, expected: nil, }, // Rest segment tests { name: "Rest segment - match", url: "/user/foo", route: "/user/*", matches: nil, expected: &Matches{ Params: map[string]string{}, Rest: "/foo", }, }, { name: "Rest segment - match multiple segments", url: "/user/foo/bar/baz", route: "/user/*", matches: nil, expected: &Matches{ Params: map[string]string{}, Rest: "/foo/bar/baz", }, }, { name: "Rest segment - no match", url: "/user", route: "/user/*", matches: nil, expected: nil, }, // Param route with rest segment { name: "Param with rest - single segment", url: "/user/2/foo", route: "/user/:id/*", matches: nil, expected: &Matches{ Params: map[string]string{"id": "2"}, Rest: "/foo", }, }, { name: "Param with rest - multiple segments", url: "/user/2/foo/bar/bob", route: "/user/:id/*", matches: nil, expected: &Matches{ Params: map[string]string{"id": "2"}, Rest: "/foo/bar/bob", }, }, { name: "Param with rest - no match", url: "/", route: "/user/:id/*", matches: nil, expected: nil, }, // Optional param tests { name: "Optional param - empty", url: "/user", route: "/user/:id?", matches: nil, expected: &Matches{ // Check "Note 1" at the top of the file Params: map[string]string{"id": ""}, }, }, { name: "Optional param - no match base", url: "/", route: "/user/:id?", matches: nil, expected: nil, }, // Optional rest param tests (/:x*) { name: "Optional rest param - empty", url: "/user", route: "/user/:id*", matches: nil, expected: &Matches{ // Check "Note 1" at the top of the file Params: map[string]string{"id": ""}, }, }, { name: "Optional rest param - with segments", url: "/user/foo/bar", route: "/user/:id*", matches: nil, expected: &Matches{ Params: map[string]string{"id": "foo/bar"}, }, }, { name: "Optional param - no match base", url: "/", route: "/user/:id*", matches: nil, expected: nil, }, // Required rest param tests (/:x+) { name: "Required rest param - single segment", url: "/user/foo", route: "/user/:id+", matches: nil, expected: &Matches{ Params: map[string]string{"id": "foo"}, }, }, { name: "Required rest param - multiple segments", url: "/user/foo/bar", route: "/user/:id+", matches: nil, expected: &Matches{ Params: map[string]string{"id": "foo/bar"}, }, }, { name: "Required rest param - empty (should fail)", url: "/user", route: "/user/:id+", matches: nil, expected: nil, }, { name: "Required rest param - root mismatch", url: "/", route: "/user/:id+", matches: nil, expected: nil, }, // Leading/trailing slashes { name: "Leading/trailing slashes", url: "/about-late/_SEGMENT1_/_SEGMENT2_/", route: "/about-late/:seg1/:seg2/", matches: nil, expected: &Matches{ Params: map[string]string{ "seg1": "_SEGMENT1_", "seg2": "_SEGMENT2_", }, }, }, // Additional tests that are not in test/node/router-match.test.js // URL encoding tests { name: "URL encoded param", url: "/foo/bar%20baz", route: "/foo/:param", matches: nil, expected: &Matches{ Params: map[string]string{"param": "bar baz"}, }, }, { name: "URL encoded email in param", url: "/users/test%40example.com/posts", route: "/users/:userId/posts", matches: nil, expected: &Matches{ Params: map[string]string{"userId": "test@example.com"}, }, }, // Complex rest segment with encoding { name: "Rest segment with encoded parts", url: "/api/path/with%20spaces/and%2Fslashes", route: "/api/:path+", matches: nil, expected: &Matches{ Params: map[string]string{"path": "path/with spaces/and/slashes"}, }, }, // Edge cases { name: "Empty route", url: "/foo", route: "", matches: nil, expected: nil, }, { name: "Empty url with param", url: "", route: "/:param", matches: nil, expected: nil, }, { name: "Mixed required and optional params", url: "/foo/bar", route: "/:required/:optional?", matches: nil, expected: &Matches{ Params: map[string]string{"required": "foo", "optional": "bar"}, }, }, { name: "Mixed required and optional params - missing optional", url: "/foo", route: "/:required/:optional?", matches: nil, expected: &Matches{ Params: map[string]string{"required": "foo", "optional": ""}, }, }, // Test with pre-existing matches { name: "Pre-existing matches object", url: "/foo/bar", route: "/:first/:second", matches: &Matches{Params: map[string]string{"existing": "value"}}, expected: &Matches{ Params: map[string]string{ "existing": "value", "first": "foo", "second": "bar", }, }, }, // Complex nested paths { name: "Complex nested path with multiple params", url: "/api/v1/users/123/posts/456/comments", route: "/api/:version/users/:userId/posts/:postId/comments", matches: nil, expected: &Matches{ Params: map[string]string{ "version": "v1", "userId": "123", "postId": "456", }, }, }, // Test case where route is longer than URL { name: "Route longer than URL - required param missing", url: "/api", route: "/api/:version/:resource", matches: nil, expected: nil, }, { name: "Route longer than URL - optional param", url: "/api", route: "/api/:version?", matches: nil, expected: &Matches{ Params: map[string]string{"version": ""}, }, }, { name: "Multiple slashes in URL should be normalized", url: "//user//123//", route: "/user/:id", matches: nil, expected: &Matches{ Params: map[string]string{"id": "123"}, }, }, { name: "Route with multiple slashes", url: "/user/123", route: "//user//:id//", matches: nil, expected: &Matches{ Params: map[string]string{"id": "123"}, }, }, { name: "Complex URL encoding in rest params", url: "/files/folder%2Fsubfolder/file%20name.txt", route: "/files/:path+", matches: nil, expected: &Matches{ Params: map[string]string{"path": "folder/subfolder/file name.txt"}, }, }, { name: "Special characters encoded in URL", url: "/search/query%3F%2B%23%26test", route: "/search/:query", matches: nil, expected: &Matches{ Params: map[string]string{"query": "query?+#&test"}, }, }, { name: "Unicode characters encoded", url: "/user/Jos%C3%A9", route: "/user/:name", matches: nil, expected: &Matches{ Params: map[string]string{"name": "José"}, }, }, { name: "Empty segments in middle of URL", url: "/api//v1//users", route: "/api/v1/users", matches: nil, expected: &Matches{ Params: map[string]string{}, }, }, { name: "Route with only wildcards", url: "/anything/goes/here", route: "*", matches: nil, expected: &Matches{ Params: map[string]string{}, Rest: "/anything/goes/here", }, }, } // URL decoding error handling tests urlDecodingTests := []struct { name string url string route string matches *Matches }{ { name: "Malformed percent encoding in simple param - should not crash", url: "/user/test%", route: "/user/:id", matches: nil, }, { name: "Malformed percent encoding in rest param - should not crash", url: "/files/test%/file", route: "/files/:path+", matches: nil, }, { name: "Invalid unicode sequence - should not crash", url: "/user/test%C3", route: "/user/:id", matches: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := preactIsoUrlPatternMatch(tt.url, tt.route, tt.matches) if tt.expected == nil { if result != nil { t.Errorf("Expected nil, got %+v", result) } return } if result == nil { t.Errorf("Expected %+v, got nil", tt.expected) return } // Detailed debugging for failing tests if !reflect.DeepEqual(result.Params, tt.expected.Params) { t.Errorf("Params mismatch for url=%q route=%q", tt.url, tt.route) t.Errorf(" Expected: %+v", tt.expected.Params) t.Errorf(" Got: %+v", result.Params) // Additional debug info for rest param cases if strings.Contains(tt.route, "+") || strings.Contains(tt.route, "*") { urlParts := filterEmpty(strings.Split(tt.url, "/")) routeParts := filterEmpty(strings.Split(tt.route, "/")) t.Errorf(" Debug: urlParts=%v, routeParts=%v", urlParts, routeParts) } } // Check rest if result.Rest != tt.expected.Rest { t.Errorf("Rest mismatch. Expected %q, got %q", tt.expected.Rest, result.Rest) } }) } // Test URL decoding error handling - these should not crash for _, tt := range urlDecodingTests { t.Run(tt.name, func(t *testing.T) { // The main requirement is that this doesn't crash // We don't care about the exact return value as long as it doesn't panic result := preactIsoUrlPatternMatch(tt.url, tt.route, tt.matches) // Should either work or return nil, but not crash if result != nil { // If it returns a result, just verify it has params if result.Params == nil { t.Errorf("Result should have non-nil Params map") } } }) } } // Debug helper to trace execution func TestDebugSpecificCase(t *testing.T) { t.Run("Debug rest param multiple segments", func(t *testing.T) { url := "/user/foo/bar" route := "/user/:id*" t.Logf("Testing: url=%q route=%q", url, route) // Manual trace of what should happen: urlParts := filterEmpty(strings.Split(url, "/")) routeParts := filterEmpty(strings.Split(route, "/")) t.Logf("urlParts: %v", urlParts) t.Logf("routeParts: %v", routeParts) t.Logf("Expected at i=1: should take urlParts[1:] = %v", urlParts[1:]) t.Logf("Expected result: %q", strings.Join(urlParts[1:], "/")) result := preactIsoUrlPatternMatch(url, route, nil) if result != nil { t.Logf("Actual result: %+v", result) } else { t.Logf("Actual result: nil") } }) } func TestFilterEmpty(t *testing.T) { tests := []struct { name string input []string expected []string }{ { name: "empty slice", input: []string{}, expected: []string{}, }, { name: "no empty strings", input: []string{"a", "b", "c"}, expected: []string{"a", "b", "c"}, }, { name: "with empty strings", input: []string{"", "a", "", "b", ""}, expected: []string{"a", "b"}, }, { name: "all empty strings", input: []string{"", "", ""}, expected: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := filterEmpty(tt.input) // Handle nil slice vs empty slice comparison if len(result) == 0 && len(tt.expected) == 0 { return // Both are effectively empty } if !reflect.DeepEqual(result, tt.expected) { t.Errorf("Expected %+v, got %+v", tt.expected, result) } }) } } ================================================ FILE: polyglot-utils/php/README.md ================================================ # PHP Implementation URL pattern matching utility for PHP servers. ## Setup Code tested on PHP 8.3.x. ```sh php --version # Ensure PHP 7.0+ is available # No third party dependencies needed. Just run the tests or use the function directly ``` ## Running Tests ```sh php test_preact_iso_url_pattern.php ``` ## Usage ```php userId . "\n"; // Output: test@example.com } ?> ``` ## Function Signature ```php function preactIsoUrlPatternMatch($url, $route, $matches = null): array|null ``` ### Parameters - `$url` (string): The URL path to match - `$route` (string): The route pattern with parameters - `$matches` (array, optional): Pre-existing matches array to extend ### Return Value Returns an array on success, or `null` if no match: ```php [ 'params' => (object)['userId' => '123'], 'userId' => '123', 'rest' => '/additional/path' // Optional ] ``` **Note**: The `params` property is a PHP object (stdClass) to maintain consistency with JSON serialization, while the outer structure is an array. ## Route Patterns | Pattern | Description | Example Result | |---------|-------------|----------------| | `/users/:id` | Named parameter | `['params' => (object)['id' => '123'], 'id' => '123']` | | `/users/:id?` | Optional parameter | `['params' => (object)['id' => null], 'id' => null]` | | `/files/:path+` | Required rest parameter | `['params' => (object)['path' => 'docs/readme.txt']]` | | `/static/:path*` | Optional rest parameter | `['params' => (object)['path' => 'css/main.css']]` | | `/static/*` | Anonymous wildcard | `['params' => (object)[], 'rest' => '/images/logo.png']` | ================================================ FILE: polyglot-utils/php/preact-iso-url-pattern.php ================================================ (object)[]]; } $url = array_values(array_filter(explode('/', $url))); $route = array_values(array_filter(explode('/', $route ?? ''))); for ($i = 0; $i < max(count($url), count($route)); $i++) { preg_match('/^(:?)(.*?)([+*?]?)$/', $route[$i] ?? '', $parts); $m = $parts[1] ?? ''; $param = $parts[2] ?? ''; $flag = $parts[3] ?? ''; $val = $url[$i] ?? null; // segment match: if (!$m && $param === $val) continue; // /foo/* match if (!$m && $val && $flag == '*') { $decodedParts = array_map('safeUrldecode', array_slice($url, $i)); $matches['rest'] = '/' . implode('/', $decodedParts); break; } // segment mismatch / missing required field: if (!$m || (!$val && $flag != '?' && $flag != '*')) { return null; } $rest = $flag == '+' || $flag == '*'; // rest (+/*) match: if ($rest) { $decodedParts = array_map('safeUrldecode', array_slice($url, $i)); $val = implode('/', $decodedParts) ?: null; } // normal/optional field: elseif ($val) { $val = safeUrldecode($url[$i]); } $matches['params']->$param = $val; if (!isset($matches[$param])) { $matches[$param] = $val; } if ($rest) break; } return $matches; } // Example usage: // var_dump(preactIsoUrlPatternMatch("/foo/bar%20baz", "/foo/:param")); // var_dump(preactIsoUrlPatternMatch("/foo/bar/baz", "/foo/*")); // var_dump(preactIsoUrlPatternMatch("/foo", "/foo/:param?")); // var_dump(preactIsoUrlPatternMatch("/foo/bar", "/bar/:param")); // var_dump(preactIsoUrlPatternMatch('/users/test%40example.com/posts', '/users/:userId/posts')); ?> ================================================ FILE: polyglot-utils/php/test_preact_iso_url_pattern.php ================================================ runTest($method); } } echo "\n" . str_repeat("=", 60) . "\n"; echo "Test Results: {$this->passed} passed, {$this->failed} failed, {$this->tests} total\n"; if ($this->failed > 0) { exit(1); } echo "All tests passed!\n"; } private function runTest($methodName) { $this->tests++; try { $this->$methodName(); $this->passed++; echo "."; } catch (Exception $e) { $this->failed++; echo "F"; echo "\nFAILED: $methodName - " . $e->getMessage() . "\n"; } } private function assertEqual($expected, $actual, $message = '') { // Use JSON comparison for deep equality check (works for arrays and objects) $expectedJson = json_encode($expected); $actualJson = json_encode($actual); if ($expectedJson !== $actualJson) { $expectedStr = json_encode($expected, JSON_PRETTY_PRINT); $actualStr = json_encode($actual, JSON_PRETTY_PRINT); throw new Exception("Expected:\n$expectedStr\nGot:\n$actualStr\n$message"); } } private function assertNull($actual, $message = '') { if ($actual !== null) { $actualStr = json_encode($actual, JSON_PRETTY_PRINT); throw new Exception("Expected null, got:\n$actualStr\n$message"); } } private function assertNotNull($actual, $message = '') { if ($actual === null) { throw new Exception("Expected non-null value, got null\n$message"); } } // Test methods start here // Base route tests public function test_base_route_exact_match() { $result = preactIsoUrlPatternMatch("/", "/"); $expected = ['params' => (object)[]]; $this->assertEqual($expected, $result); } public function test_base_route_no_match() { $result = preactIsoUrlPatternMatch("/user/1", "/"); $this->assertNull($result); } // Param route tests public function test_param_route_match() { $result = preactIsoUrlPatternMatch("/user/2", "/user/:id"); $expected = ['params' => (object)['id' => '2'], 'id' => '2']; $this->assertEqual($expected, $result); } public function test_param_route_no_match() { $result = preactIsoUrlPatternMatch("/", "/user/:id"); $this->assertNull($result); } // Rest segment tests public function test_rest_segment_match() { $result = preactIsoUrlPatternMatch("/user/foo", "/user/*"); $expected = ['params' => (object)[], 'rest' => '/foo']; $this->assertEqual($expected, $result); } public function test_rest_segment_match_multiple_segments() { $result = preactIsoUrlPatternMatch("/user/foo/bar/baz", "/user/*"); $expected = ['params' => (object)[], 'rest' => '/foo/bar/baz']; $this->assertEqual($expected, $result); } public function test_rest_segment_no_match() { $result = preactIsoUrlPatternMatch("/user", "/user/*"); $this->assertNull($result); } public function test_rest_segment_no_match_different_case() { $result = preactIsoUrlPatternMatch("/", "/user/:id/*"); $this->assertNull($result); } // Param route with rest segment public function test_param_with_rest_single_segment() { $result = preactIsoUrlPatternMatch("/user/2/foo", "/user/:id/*"); $expected = ['params' => (object)['id' => '2'], 'id' => '2', 'rest' => '/foo']; $this->assertEqual($expected, $result); } public function test_param_with_rest_multiple_segments() { $result = preactIsoUrlPatternMatch("/user/2/foo/bar/bob", "/user/:id/*"); $expected = ['params' => (object)['id' => '2'], 'id' => '2', 'rest' => '/foo/bar/bob']; $this->assertEqual($expected, $result); } public function test_param_with_rest_no_match() { $result = preactIsoUrlPatternMatch("/", "/user/:id/*"); $this->assertNull($result); } // Optional param tests public function test_optional_param_empty() { $result = preactIsoUrlPatternMatch("/user", "/user/:id?"); $expected = ['params' => (object)['id' => null], 'id' => null]; $this->assertEqual($expected, $result); } public function test_optional_param_no_match_base() { $result = preactIsoUrlPatternMatch("/", "/user/:id?"); $this->assertNull($result); } // Optional rest param tests (/:x*) public function test_optional_rest_param_empty() { $result = preactIsoUrlPatternMatch("/user", "/user/:id*"); $expected = ['params' => (object)['id' => null], 'id' => null]; $this->assertEqual($expected, $result); } public function test_optional_rest_param_with_segments() { $result = preactIsoUrlPatternMatch("/user/foo/bar", "/user/:id*"); $expected = ['params' => (object)['id' => 'foo/bar'], 'id' => 'foo/bar']; $this->assertEqual($expected, $result); } public function test_optional_param_no_match_base_duplicate() { $result = preactIsoUrlPatternMatch("/", "/user/:id*"); $this->assertNull($result); } // Required rest param tests (/:x+) public function test_required_rest_param_single_segment() { $result = preactIsoUrlPatternMatch("/user/foo", "/user/:id+"); $expected = ['params' => (object)['id' => 'foo'], 'id' => 'foo']; $this->assertEqual($expected, $result); } public function test_required_rest_param_multiple_segments() { $result = preactIsoUrlPatternMatch("/user/foo/bar", "/user/:id+"); $expected = ['params' => (object)['id' => 'foo/bar'], 'id' => 'foo/bar']; $this->assertEqual($expected, $result); } public function test_required_rest_param_empty_should_fail() { $result = preactIsoUrlPatternMatch("/user", "/user/:id+"); $this->assertNull($result); } public function test_required_rest_param_root_mismatch() { $result = preactIsoUrlPatternMatch("/", "/user/:id+"); $this->assertNull($result); } // Leading/trailing slashes public function test_leading_trailing_slashes() { $result = preactIsoUrlPatternMatch("/about-late/_SEGMENT1_/_SEGMENT2_/", "/about-late/:seg1/:seg2/"); $expected = ['params' => (object)['seg1' => '_SEGMENT1_', 'seg2' => '_SEGMENT2_'], 'seg1' => '_SEGMENT1_', 'seg2' => '_SEGMENT2_']; $this->assertEqual($expected, $result); } // Additional tests that are not in test/node/router-match.test.js // URL encoding tests public function test_url_encoded_param() { $result = preactIsoUrlPatternMatch("/foo/bar%20baz", "/foo/:param"); $expected = ['params' => (object)['param' => 'bar baz'], 'param' => 'bar baz']; $this->assertEqual($expected, $result); } public function test_url_encoded_email_in_param() { $result = preactIsoUrlPatternMatch("/users/test%40example.com/posts", "/users/:userId/posts"); $expected = ['params' => (object)['userId' => 'test@example.com'], 'userId' => 'test@example.com']; $this->assertEqual($expected, $result); } // Complex rest segment with encoding public function test_rest_segment_with_encoded_parts() { $result = preactIsoUrlPatternMatch("/api/path/with%20spaces/and%2Fslashes", "/api/:path+"); $expected = ['params' => (object)['path' => 'path/with spaces/and/slashes'], 'path' => 'path/with spaces/and/slashes']; $this->assertEqual($expected, $result); } // Edge cases public function test_empty_route() { $result = preactIsoUrlPatternMatch("/foo", ""); $this->assertNull($result); } public function test_empty_url_with_param() { $result = preactIsoUrlPatternMatch("", "/:param"); $this->assertNull($result); } public function test_mixed_required_and_optional_params() { $result = preactIsoUrlPatternMatch("/foo/bar", "/:required/:optional?"); $expected = ['params' => (object)['required' => 'foo', 'optional' => 'bar'], 'required' => 'foo', 'optional' => 'bar']; $this->assertEqual($expected, $result); } public function test_mixed_required_and_optional_params_missing_optional() { $result = preactIsoUrlPatternMatch("/foo", "/:required/:optional?"); $expected = ['params' => (object)['required' => 'foo', 'optional' => null], 'required' => 'foo', 'optional' => null]; $this->assertEqual($expected, $result); } // Test with pre-existing matches public function test_pre_existing_matches_object() { $matches = ['params' => (object)['existing' => 'value']]; $result = preactIsoUrlPatternMatch("/foo/bar", "/:first/:second", $matches); $expected = ['params' => (object)['existing' => 'value', 'first' => 'foo', 'second' => 'bar'], 'first' => 'foo', 'second' => 'bar']; $this->assertEqual($expected, $result); } // Complex nested paths public function test_complex_nested_path_with_multiple_params() { $result = preactIsoUrlPatternMatch("/api/v1/users/123/posts/456/comments", "/api/:version/users/:userId/posts/:postId/comments"); $expected = [ 'params' => (object)['version' => 'v1', 'userId' => '123', 'postId' => '456'], 'version' => 'v1', 'userId' => '123', 'postId' => '456' ]; $this->assertEqual($expected, $result); } public function test_route_longer_than_url_required_param_missing() { $result = preactIsoUrlPatternMatch("/api", "/api/:version/:resource"); $this->assertNull($result); } public function test_route_longer_than_url_optional_param() { $result = preactIsoUrlPatternMatch("/api", "/api/:version?"); $expected = ['params' => (object)['version' => null], 'version' => null]; $this->assertEqual($expected, $result); } public function test_multiple_slashes_in_url_should_be_normalized() { $result = preactIsoUrlPatternMatch("//user//123//", "/user/:id"); $expected = ['params' => (object)['id' => '123'], 'id' => '123']; $this->assertEqual($expected, $result); } public function test_route_with_multiple_slashes() { $result = preactIsoUrlPatternMatch("/user/123", "//user//:id//"); $expected = ['params' => (object)['id' => '123'], 'id' => '123']; $this->assertEqual($expected, $result); } // Additional URL encoding tests public function test_complex_url_encoding_in_rest_params() { $result = preactIsoUrlPatternMatch("/files/folder%2Fsubfolder/file%20name.txt", "/files/:path+"); $expected = ['params' => (object)['path' => 'folder/subfolder/file name.txt'], 'path' => 'folder/subfolder/file name.txt']; $this->assertEqual($expected, $result); } public function test_special_characters_encoded_in_url() { $result = preactIsoUrlPatternMatch("/search/query%3F%2B%23%26test", "/search/:query"); $expected = ['params' => (object)['query' => 'query?+#&test'], 'query' => 'query?+#&test']; $this->assertEqual($expected, $result); } public function test_unicode_characters_encoded() { $result = preactIsoUrlPatternMatch("/user/Jos%C3%A9", "/user/:name"); $expected = ['params' => (object)['name' => 'José'], 'name' => 'José']; $this->assertEqual($expected, $result); } public function test_empty_segments_in_middle_of_url() { $result = preactIsoUrlPatternMatch("/api//v1//users", "/api/v1/users"); $expected = ['params' => (object)[]]; $this->assertEqual($expected, $result); } public function test_route_with_only_wildcards() { $result = preactIsoUrlPatternMatch("/anything/goes/here", "*"); $expected = ['params' => (object)[], 'rest' => '/anything/goes/here']; $this->assertEqual($expected, $result); } // URL decoding error handling tests public function test_malformed_percent_encoding_simple_param() { // Test malformed percent encoding in simple param - should not crash $result = preactIsoUrlPatternMatch("/user/test%", "/user/:id"); // Should either work or return null, but not crash $this->assertNotNull($result); } public function test_malformed_percent_encoding_rest_param() { // Test malformed percent encoding in rest param - should not crash $result = preactIsoUrlPatternMatch("/files/test%/file", "/files/:path+"); // Should either work or return null, but not crash $this->assertNotNull($result); } public function test_invalid_unicode_sequence() { // Test invalid unicode sequence - should not crash $result = preactIsoUrlPatternMatch("/user/test%C3", "/user/:id"); // Should either work or return null, but not crash $this->assertNotNull($result); } } // Run tests if this file is executed directly if (php_sapi_name() === 'cli') { $test = new TestPreactIsoUrlPatternMatch(); $test->run(); } ?> ================================================ FILE: polyglot-utils/python/README.md ================================================ # Python Implementation URL pattern matching utility for Python servers. ## Setup Code tested on Python 3.12.x. No external dependencies required - uses only Python standard library. ```sh python3 --version # Ensure Python 3.6+ is available # No third party dependencies needed. Just run the tests or use the function directly ``` ## Running Tests ```sh python3 test_preact_iso_url_pattern.py ``` ## Usage ```python from preact_iso_url_pattern import preact_iso_url_pattern_match matches = preact_iso_url_pattern_match("/users/test%40example.com/posts", "/users/:userId/posts") if matches: print(f"User ID: {matches['params']['userId']}") # Output: test@example.com ``` ## Function Signature ```python def preact_iso_url_pattern_match(url, route, matches=None) -> dict | None ``` ### Parameters - `url` (str): The URL path to match - `route` (str): The route pattern with parameters - `matches` (dict, optional): Pre-existing matches dictionary to extend ### Return Value Returns a dictionary on success, or `None` if no match: ```python { "params": {"userId": "123"}, "userId": "123", "rest": "/additional/path" # Optional } ``` ## Route Patterns | Pattern | Description | Example Result | |---------|-------------|----------------| | `/users/:id` | Named parameter | `{"params": {"id": "123"}, "id": "123"}` | | `/users/:id?` | Optional parameter | `{"params": {"id": None}, "id": None}` | | `/files/:path+` | Required rest parameter | `{"params": {"path": "docs/readme.txt"}}` | | `/static/:path*` | Optional rest parameter | `{"params": {"path": "css/main.css"}}` | | `/static/*` | Anonymous wildcard | `{"params": {}, "rest": "/images/logo.png"}` | ================================================ FILE: polyglot-utils/python/preact_iso_url_pattern.py ================================================ # Run program: python3 preact-iso-url-pattern.py from urllib.parse import unquote # Safe URL decode function with error handling def safe_unquote(s): if s is None or s == '': return s try: return unquote(s) except UnicodeDecodeError: # If unquote fails due to malformed encoding, return original string return s def preact_iso_url_pattern_match(url, route, matches=None): # Initialize matches object if not provided if matches is None: matches = {'params': {}} url = list(filter(None, url.split('/'))) route = list(filter(None, (route or '').split('/'))) for i in range(max(len(url), len(route))): m, param, flag = '', '', '' if i < len(route): parts = route[i].split(':') m = ':' if len(parts) > 1 else '' param = parts[-1] flag = '' if param and param[-1] in '+*?': flag = param[-1] param = param[:-1] val = url[i] if i < len(url) else None # segment match: if not m and param == val: continue # /foo/* match if not m and val and flag == '*': # Store remaining path segments in rest matches['rest'] = '/' + '/'.join(map(safe_unquote, url[i:])) break # segment mismatch / missing required field: if not m or (not val and flag != '?' and flag != '*'): return None rest = flag in ('+', '*') # rest (+/*) match: if rest: val = '/'.join(map(safe_unquote, url[i:])) or None # normal/optional field: elif val: val = safe_unquote(val) # Store parameter values in matches matches['params'][param] = val if param not in matches: matches[param] = val if rest: break return matches # Example usage: # print(preact_iso_url_pattern_match("/foo/bar%20baz", "/foo/:param")) # print(preact_iso_url_pattern_match("/foo/bar/baz", "/foo/*")) # print(preact_iso_url_pattern_match("/foo", "/foo/:param?")) # print(preact_iso_url_pattern_match("/foo/bar", "/bar/:param")) # print(preact_iso_url_pattern_match('/users/test%40example.com/posts', '/users/:userId/posts')) ================================================ FILE: polyglot-utils/python/test_preact_iso_url_pattern.py ================================================ #!/usr/bin/env python3 """Test suite for preact_iso_url_pattern.py - ported from Go tests""" import unittest from preact_iso_url_pattern import preact_iso_url_pattern_match class TestPreactIsoUrlPatternMatch(unittest.TestCase): # Base route tests def test_base_route_exact_match(self): """Base route - exact match""" result = preact_iso_url_pattern_match("/", "/") expected = {'params': {}} self.assertEqual(result, expected) def test_base_route_no_match(self): """Base route - no match""" result = preact_iso_url_pattern_match("/user/1", "/") self.assertIsNone(result) # Param route tests def test_param_route_match(self): """Param route - match""" result = preact_iso_url_pattern_match("/user/2", "/user/:id") expected = {'params': {'id': '2'}, 'id': '2'} self.assertEqual(result, expected) def test_param_route_no_match(self): """Param route - no match""" result = preact_iso_url_pattern_match("/", "/user/:id") self.assertIsNone(result) # Rest segment tests def test_rest_segment_match(self): """Rest segment - match""" result = preact_iso_url_pattern_match("/user/foo", "/user/*") expected = {'params': {}, 'rest': '/foo'} self.assertEqual(result, expected) def test_rest_segment_match_multiple_segments(self): """Rest segment - match multiple segments""" result = preact_iso_url_pattern_match("/user/foo/bar/baz", "/user/*") expected = {'params': {}, 'rest': '/foo/bar/baz'} self.assertEqual(result, expected) def test_rest_segment_no_match(self): """Rest segment - no match""" result = preact_iso_url_pattern_match("/user", "/user/*") self.assertIsNone(result) def test_rest_segment_no_match_different_case(self): """Rest segment - no match different case""" result = preact_iso_url_pattern_match("/", "/user/:id/*") self.assertIsNone(result) # Param route with rest segment def test_param_with_rest_single_segment(self): """Param with rest - single segment""" result = preact_iso_url_pattern_match("/user/2/foo", "/user/:id/*") expected = {'params': {'id': '2'}, 'id': '2', 'rest': '/foo'} self.assertEqual(result, expected) def test_param_with_rest_multiple_segments(self): """Param with rest - multiple segments""" result = preact_iso_url_pattern_match("/user/2/foo/bar/bob", "/user/:id/*") expected = {'params': {'id': '2'}, 'id': '2', 'rest': '/foo/bar/bob'} self.assertEqual(result, expected) def test_param_with_rest_no_match(self): """Param with rest - no match""" result = preact_iso_url_pattern_match("/", "/user/:id/*") self.assertIsNone(result) # Optional param tests def test_optional_param_empty(self): """Optional param - empty""" result = preact_iso_url_pattern_match("/user", "/user/:id?") expected = {'params': {'id': None}, 'id': None} self.assertEqual(result, expected) def test_optional_param_no_match_base(self): """Optional param - no match base""" result = preact_iso_url_pattern_match("/", "/user/:id?") self.assertIsNone(result) # Optional rest param tests (/:x*) def test_optional_rest_param_empty(self): """Optional rest param - empty""" result = preact_iso_url_pattern_match("/user", "/user/:id*") expected = {'params': {'id': None}, 'id': None} self.assertEqual(result, expected) def test_optional_rest_param_with_segments(self): """Optional rest param - with segments""" result = preact_iso_url_pattern_match("/user/foo/bar", "/user/:id*") expected = {'params': {'id': 'foo/bar'}, 'id': 'foo/bar'} self.assertEqual(result, expected) def test_optional_param_no_match_base_duplicate(self): """Optional param - no match base duplicate""" result = preact_iso_url_pattern_match("/", "/user/:id*") self.assertIsNone(result) # Required rest param tests (/:x+) def test_required_rest_param_single_segment(self): """Required rest param - single segment""" result = preact_iso_url_pattern_match("/user/foo", "/user/:id+") expected = {'params': {'id': 'foo'}, 'id': 'foo'} self.assertEqual(result, expected) def test_required_rest_param_multiple_segments(self): """Required rest param - multiple segments""" result = preact_iso_url_pattern_match("/user/foo/bar", "/user/:id+") expected = {'params': {'id': 'foo/bar'}, 'id': 'foo/bar'} self.assertEqual(result, expected) def test_required_rest_param_empty_should_fail(self): """Required rest param - empty (should fail)""" result = preact_iso_url_pattern_match("/user", "/user/:id+") self.assertIsNone(result) def test_required_rest_param_root_mismatch(self): """Required rest param - root mismatch""" result = preact_iso_url_pattern_match("/", "/user/:id+") self.assertIsNone(result) # Leading/trailing slashes def test_leading_trailing_slashes(self): """Leading/trailing slashes""" result = preact_iso_url_pattern_match("/about-late/_SEGMENT1_/_SEGMENT2_/", "/about-late/:seg1/:seg2/") expected = {'params': {'seg1': '_SEGMENT1_', 'seg2': '_SEGMENT2_'}, 'seg1': '_SEGMENT1_', 'seg2': '_SEGMENT2_'} self.assertEqual(result, expected) # Additional tests that are not in test/node/router-match.test.js # URL encoding tests def test_url_encoded_param(self): """URL encoded param""" result = preact_iso_url_pattern_match("/foo/bar%20baz", "/foo/:param") expected = {'params': {'param': 'bar baz'}, 'param': 'bar baz'} self.assertEqual(result, expected) def test_url_encoded_email_in_param(self): """URL encoded email in param""" result = preact_iso_url_pattern_match("/users/test%40example.com/posts", "/users/:userId/posts") expected = {'params': {'userId': 'test@example.com'}, 'userId': 'test@example.com'} self.assertEqual(result, expected) # Complex rest segment with encoding def test_rest_segment_with_encoded_parts(self): """Rest segment with encoded parts""" result = preact_iso_url_pattern_match("/api/path/with%20spaces/and%2Fslashes", "/api/:path+") expected = {'params': {'path': 'path/with spaces/and/slashes'}, 'path': 'path/with spaces/and/slashes'} self.assertEqual(result, expected) # Edge cases def test_empty_route(self): """Empty route""" result = preact_iso_url_pattern_match("/foo", "") self.assertIsNone(result) def test_empty_url_with_param(self): """Empty url with param""" result = preact_iso_url_pattern_match("", "/:param") self.assertIsNone(result) def test_mixed_required_and_optional_params(self): """Mixed required and optional params""" result = preact_iso_url_pattern_match("/foo/bar", "/:required/:optional?") expected = {'params': {'required': 'foo', 'optional': 'bar'}, 'required': 'foo', 'optional': 'bar'} self.assertEqual(result, expected) def test_mixed_required_and_optional_params_missing_optional(self): """Mixed required and optional params - missing optional""" result = preact_iso_url_pattern_match("/foo", "/:required/:optional?") expected = {'params': {'required': 'foo', 'optional': None}, 'required': 'foo', 'optional': None} self.assertEqual(result, expected) # Test with pre-existing matches def test_pre_existing_matches_object(self): """Pre-existing matches object""" matches = {'params': {'existing': 'value'}} result = preact_iso_url_pattern_match("/foo/bar", "/:first/:second", matches) expected = {'params': {'existing': 'value', 'first': 'foo', 'second': 'bar'}, 'first': 'foo', 'second': 'bar'} self.assertEqual(result, expected) # Complex nested paths def test_complex_nested_path_with_multiple_params(self): """Complex nested path with multiple params""" result = preact_iso_url_pattern_match("/api/v1/users/123/posts/456/comments", "/api/:version/users/:userId/posts/:postId/comments") expected = { 'params': {'version': 'v1', 'userId': '123', 'postId': '456'}, 'version': 'v1', 'userId': '123', 'postId': '456' } self.assertEqual(result, expected) def test_route_longer_than_url_required_param_missing(self): """Route longer than URL - required param missing""" result = preact_iso_url_pattern_match("/api", "/api/:version/:resource") self.assertIsNone(result) def test_route_longer_than_url_optional_param(self): """Route longer than URL - optional param""" result = preact_iso_url_pattern_match("/api", "/api/:version?") expected = {'params': {'version': None}, 'version': None} self.assertEqual(result, expected) def test_multiple_slashes_in_url_should_be_normalized(self): """Multiple slashes in URL should be normalized""" result = preact_iso_url_pattern_match("//user//123//", "/user/:id") expected = {'params': {'id': '123'}, 'id': '123'} self.assertEqual(result, expected) def test_route_with_multiple_slashes(self): """Route with multiple slashes""" result = preact_iso_url_pattern_match("/user/123", "//user//:id//") expected = {'params': {'id': '123'}, 'id': '123'} self.assertEqual(result, expected) def test_complex_url_encoding_in_rest_params(self): """Complex URL encoding in rest params""" result = preact_iso_url_pattern_match("/files/folder%2Fsubfolder/file%20name.txt", "/files/:path+") expected = {'params': {'path': 'folder/subfolder/file name.txt'}, 'path': 'folder/subfolder/file name.txt'} self.assertEqual(result, expected) def test_special_characters_encoded_in_url(self): """Special characters encoded in URL""" result = preact_iso_url_pattern_match("/search/query%3F%2B%23%26test", "/search/:query") expected = {'params': {'query': 'query?+#&test'}, 'query': 'query?+#&test'} self.assertEqual(result, expected) def test_unicode_characters_encoded(self): """Unicode characters encoded""" result = preact_iso_url_pattern_match("/user/Jos%C3%A9", "/user/:name") expected = {'params': {'name': 'José'}, 'name': 'José'} self.assertEqual(result, expected) def test_empty_segments_in_middle_of_url(self): """Empty segments in middle of URL""" result = preact_iso_url_pattern_match("/api//v1//users", "/api/v1/users") expected = {'params': {}} self.assertEqual(result, expected) def test_route_with_only_wildcards(self): """Route with only wildcards""" result = preact_iso_url_pattern_match("/anything/goes/here", "*") expected = {'params': {}, 'rest': '/anything/goes/here'} self.assertEqual(result, expected) class TestUrlDecodingErrorHandling(unittest.TestCase): """Tests specifically for URL decoding error scenarios""" def test_malformed_percent_encoding_simple_param(self): """Test malformed percent encoding in simple param - should not crash""" # This should handle malformed encoding gracefully result = preact_iso_url_pattern_match("/user/test%", "/user/:id") # Should either work or return None, but not crash self.assertIsNotNone(result) def test_malformed_percent_encoding_rest_param(self): """Test malformed percent encoding in rest param - should not crash""" result = preact_iso_url_pattern_match("/files/test%/file", "/files/:path+") # Should either work or return None, but not crash self.assertIsNotNone(result) def test_invalid_unicode_sequence(self): """Test invalid unicode sequence - should not crash""" result = preact_iso_url_pattern_match("/user/test%C3", "/user/:id") # Should either work or return None, but not crash self.assertIsNotNone(result) if __name__ == '__main__': # Run tests with verbose output unittest.main(verbosity=2) ================================================ FILE: polyglot-utils/ruby/README.md ================================================ # Ruby Implementation URL pattern matching utility for Ruby servers. ## Setup Code tested on ruby 3.2.x. ```sh ruby --version # Ensure Ruby 2.0+ is available # No third party dependencies needed. Just run the tests or use the function directly ``` ## Running Tests ```sh ruby test_preact_iso_url_pattern.rb ``` ## Usage ```ruby require_relative 'preact-iso-url-pattern' matches = preact_iso_url_pattern_match("/users/test%40example.com/posts", "/users/:userId/posts") if matches puts "User ID: #{matches['params']['userId']}" # Output: test@example.com end ``` ## Function Signature ```ruby def preact_iso_url_pattern_match(url, route, matches = nil) -> Hash | nil ``` ### Parameters - `url` (String): The URL path to match - `route` (String): The route pattern with parameters - `matches` (Hash, optional): Pre-existing matches hash to extend ### Return Value Returns a Hash on success, or `nil` if no match: ```ruby { 'params' => { 'userId' => '123' }, 'userId' => '123', 'rest' => '/additional/path' # Optional } ``` ## Route Patterns | Pattern | Description | Example Result | |---------|-------------|----------------| | `/users/:id` | Named parameter | `{'params' => {'id' => '123'}, 'id' => '123'}` | | `/users/:id?` | Optional parameter | `{'params' => {'id' => nil}, 'id' => nil}` | | `/files/:path+` | Required rest parameter | `{'params' => {'path' => 'docs/readme.txt'}}` | | `/static/:path*` | Optional rest parameter | `{'params' => {'path' => 'css/main.css'}}` | | `/static/*` | Anonymous wildcard | `{'params' => {}, 'rest' => '/images/logo.png'}` | ================================================ FILE: polyglot-utils/ruby/preact-iso-url-pattern.rb ================================================ # Run program: ruby preact-iso-url-pattern.rb require 'cgi' # Safe URL decode function with error handling def safe_cgi_unescape(str) return str if str.nil? || str.empty? begin CGI.unescape(str) rescue ArgumentError # If CGI.unescape fails due to malformed encoding, return original string str end end def preact_iso_url_pattern_match(url, route, matches = nil) matches ||= { 'params' => {} } url = url.split('/').reject(&:empty?) route = (route || '').split('/').reject(&:empty?) (0...[url.length, route.length].max).each do |i| m, param, flag = route[i]&.match(/^(:?)(.*?)([+*?]?)$/)&.captures || ['', '', ''] val = url[i] # segment match: next if m.empty? && param == val # /foo/* match if m.empty? && val && flag == '*' decoded_parts = url[i..].map { |part| safe_cgi_unescape(part) } matches['rest'] = '/' + decoded_parts.join('/') break end # segment mismatch / missing required field: return nil if m.empty? || (!val && flag != '?' && flag != '*') rest = flag == '+' || flag == '*' # rest (+/*) match: if rest decoded_parts = url[i..].map { |part| safe_cgi_unescape(part) } joined = decoded_parts.join('/') val = joined.empty? ? nil : joined # normal/optional field: elsif val val = safe_cgi_unescape(val) end matches['params'][param] = val matches[param] = val unless matches.key?(param) break if rest end matches end # Example usage: # puts preact_iso_url_pattern_match("/foo/bar%20baz", "/foo/:param") # puts preact_iso_url_pattern_match("/foo/bar/baz", "/foo/*") # puts preact_iso_url_pattern_match("/foo", "/foo/:param?") # puts preact_iso_url_pattern_match("/foo/bar", "/bar/:param") # puts preact_iso_url_pattern_match('/users/test%40example.com/posts', '/users/:userId/posts') ================================================ FILE: polyglot-utils/ruby/test_preact_iso_url_pattern.rb ================================================ #!/usr/bin/env ruby # Test suite for preact-iso-url-pattern.rb - ported from Go tests require 'minitest/autorun' require_relative 'preact-iso-url-pattern' class TestPreactIsoUrlPatternMatch < Minitest::Test # Base route tests def test_base_route_exact_match # Base route - exact match result = preact_iso_url_pattern_match("/", "/") expected = { 'params' => {} } assert_equal expected, result end def test_base_route_no_match # Base route - no match result = preact_iso_url_pattern_match("/user/1", "/") assert_nil result end # Param route tests def test_param_route_match # Param route - match result = preact_iso_url_pattern_match("/user/2", "/user/:id") expected = { 'params' => { 'id' => '2' }, 'id' => '2' } assert_equal expected, result end def test_param_route_no_match # Param route - no match result = preact_iso_url_pattern_match("/", "/user/:id") assert_nil result end # Rest segment tests def test_rest_segment_match # Rest segment - match result = preact_iso_url_pattern_match("/user/foo", "/user/*") expected = { 'params' => {}, 'rest' => '/foo' } assert_equal expected, result end def test_rest_segment_match_multiple_segments # Rest segment - match multiple segments result = preact_iso_url_pattern_match("/user/foo/bar/baz", "/user/*") expected = { 'params' => {}, 'rest' => '/foo/bar/baz' } assert_equal expected, result end def test_rest_segment_no_match # Rest segment - no match result = preact_iso_url_pattern_match("/user", "/user/*") assert_nil result end def test_rest_segment_no_match_different_case # Rest segment - no match different case result = preact_iso_url_pattern_match("/", "/user/:id/*") assert_nil result end # Param route with rest segment def test_param_with_rest_single_segment # Param with rest - single segment result = preact_iso_url_pattern_match("/user/2/foo", "/user/:id/*") expected = { 'params' => { 'id' => '2' }, 'id' => '2', 'rest' => '/foo' } assert_equal expected, result end def test_param_with_rest_multiple_segments # Param with rest - multiple segments result = preact_iso_url_pattern_match("/user/2/foo/bar/bob", "/user/:id/*") expected = { 'params' => { 'id' => '2' }, 'id' => '2', 'rest' => '/foo/bar/bob' } assert_equal expected, result end def test_param_with_rest_no_match # Param with rest - no match result = preact_iso_url_pattern_match("/", "/user/:id/*") assert_nil result end # Optional param tests def test_optional_param_empty # Optional param - empty result = preact_iso_url_pattern_match("/user", "/user/:id?") expected = { 'params' => { 'id' => nil }, 'id' => nil } assert_equal expected, result end def test_optional_param_no_match_base # Optional param - no match base result = preact_iso_url_pattern_match("/", "/user/:id?") assert_nil result end # Optional rest param tests (/:x*) def test_optional_rest_param_empty # Optional rest param - empty result = preact_iso_url_pattern_match("/user", "/user/:id*") expected = { 'params' => { 'id' => nil }, 'id' => nil } assert_equal expected, result end def test_optional_rest_param_with_segments # Optional rest param - with segments result = preact_iso_url_pattern_match("/user/foo/bar", "/user/:id*") expected = { 'params' => { 'id' => 'foo/bar' }, 'id' => 'foo/bar' } assert_equal expected, result end def test_optional_param_no_match_base_duplicate # Optional param - no match base duplicate result = preact_iso_url_pattern_match("/", "/user/:id*") assert_nil result end # Required rest param tests (/:x+) def test_required_rest_param_single_segment # Required rest param - single segment result = preact_iso_url_pattern_match("/user/foo", "/user/:id+") expected = { 'params' => { 'id' => 'foo' }, 'id' => 'foo' } assert_equal expected, result end def test_required_rest_param_multiple_segments # Required rest param - multiple segments result = preact_iso_url_pattern_match("/user/foo/bar", "/user/:id+") expected = { 'params' => { 'id' => 'foo/bar' }, 'id' => 'foo/bar' } assert_equal expected, result end def test_required_rest_param_empty_should_fail # Required rest param - empty (should fail) result = preact_iso_url_pattern_match("/user", "/user/:id+") assert_nil result end def test_required_rest_param_root_mismatch # Required rest param - root mismatch result = preact_iso_url_pattern_match("/", "/user/:id+") assert_nil result end # Leading/trailing slashes def test_leading_trailing_slashes # Leading/trailing slashes result = preact_iso_url_pattern_match("/about-late/_SEGMENT1_/_SEGMENT2_/", "/about-late/:seg1/:seg2/") expected = { 'params' => { 'seg1' => '_SEGMENT1_', 'seg2' => '_SEGMENT2_' }, 'seg1' => '_SEGMENT1_', 'seg2' => '_SEGMENT2_' } assert_equal expected, result end # Additional tests that are not in test/node/router-match.test.js # URL encoding tests def test_url_encoded_param # URL encoded param result = preact_iso_url_pattern_match("/foo/bar%20baz", "/foo/:param") expected = { 'params' => { 'param' => 'bar baz' }, 'param' => 'bar baz' } assert_equal expected, result end def test_url_encoded_email_in_param # URL encoded email in param result = preact_iso_url_pattern_match("/users/test%40example.com/posts", "/users/:userId/posts") expected = { 'params' => { 'userId' => 'test@example.com' }, 'userId' => 'test@example.com' } assert_equal expected, result end # Complex rest segment with encoding def test_rest_segment_with_encoded_parts # Rest segment with encoded parts result = preact_iso_url_pattern_match("/api/path/with%20spaces/and%2Fslashes", "/api/:path+") expected = { 'params' => { 'path' => 'path/with spaces/and/slashes' }, 'path' => 'path/with spaces/and/slashes' } assert_equal expected, result end # Edge cases def test_empty_route # Empty route result = preact_iso_url_pattern_match("/foo", "") assert_nil result end def test_empty_url_with_param # Empty url with param result = preact_iso_url_pattern_match("", "/:param") assert_nil result end def test_mixed_required_and_optional_params # Mixed required and optional params result = preact_iso_url_pattern_match("/foo/bar", "/:required/:optional?") expected = { 'params' => { 'required' => 'foo', 'optional' => 'bar' }, 'required' => 'foo', 'optional' => 'bar' } assert_equal expected, result end def test_mixed_required_and_optional_params_missing_optional # Mixed required and optional params - missing optional result = preact_iso_url_pattern_match("/foo", "/:required/:optional?") expected = { 'params' => { 'required' => 'foo', 'optional' => nil }, 'required' => 'foo', 'optional' => nil } assert_equal expected, result end # Test with pre-existing matches def test_pre_existing_matches_object # Pre-existing matches object matches = { 'params' => { 'existing' => 'value' } } result = preact_iso_url_pattern_match("/foo/bar", "/:first/:second", matches) expected = { 'params' => { 'existing' => 'value', 'first' => 'foo', 'second' => 'bar' }, 'first' => 'foo', 'second' => 'bar' } assert_equal expected, result end # Complex nested paths def test_complex_nested_path_with_multiple_params # Complex nested path with multiple params result = preact_iso_url_pattern_match("/api/v1/users/123/posts/456/comments", "/api/:version/users/:userId/posts/:postId/comments") expected = { 'params' => { 'version' => 'v1', 'userId' => '123', 'postId' => '456' }, 'version' => 'v1', 'userId' => '123', 'postId' => '456' } assert_equal expected, result end def test_route_longer_than_url_required_param_missing # Route longer than URL - required param missing result = preact_iso_url_pattern_match("/api", "/api/:version/:resource") assert_nil result end def test_route_longer_than_url_optional_param # Route longer than URL - optional param result = preact_iso_url_pattern_match("/api", "/api/:version?") expected = { 'params' => { 'version' => nil }, 'version' => nil } assert_equal expected, result end def test_multiple_slashes_in_url_should_be_normalized # Multiple slashes in URL should be normalized result = preact_iso_url_pattern_match("//user//123//", "/user/:id") expected = { 'params' => { 'id' => '123' }, 'id' => '123' } assert_equal expected, result end def test_route_with_multiple_slashes # Route with multiple slashes result = preact_iso_url_pattern_match("/user/123", "//user//:id//") expected = { 'params' => { 'id' => '123' }, 'id' => '123' } assert_equal expected, result end def test_rest_param_with_single_character # Rest param with single character result = preact_iso_url_pattern_match("/a/b", "/:x+") expected = { 'params' => { 'x' => 'a/b' }, 'x' => 'a/b' } assert_equal expected, result end def test_complex_url_encoding_in_rest_params # Complex URL encoding in rest params result = preact_iso_url_pattern_match("/files/folder%2Fsubfolder/file%20name.txt", "/files/:path+") expected = { 'params' => { 'path' => 'folder/subfolder/file name.txt' }, 'path' => 'folder/subfolder/file name.txt' } assert_equal expected, result end def test_special_characters_encoded_in_url # Special characters encoded in URL result = preact_iso_url_pattern_match("/search/query%3F%2B%23%26test", "/search/:query") expected = { 'params' => { 'query' => 'query?+#&test' }, 'query' => 'query?+#&test' } assert_equal expected, result end def test_unicode_characters_encoded # Unicode characters encoded result = preact_iso_url_pattern_match("/user/Jos%C3%A9", "/user/:name") expected = { 'params' => { 'name' => 'José' }, 'name' => 'José' } assert_equal expected, result end def test_empty_segments_in_middle_of_url # Empty segments in middle of URL result = preact_iso_url_pattern_match("/api//v1//users", "/api/v1/users") expected = { 'params' => {} } assert_equal expected, result end def test_route_with_only_wildcards # Route with only wildcards result = preact_iso_url_pattern_match("/anything/goes/here", "*") expected = { 'params' => {}, 'rest' => '/anything/goes/here' } assert_equal expected, result end end class TestUrlDecodingErrorHandling < Minitest::Test # Tests specifically for URL decoding error scenarios def test_malformed_percent_encoding_simple_param # Test malformed percent encoding in simple param - should not crash # This should handle malformed encoding gracefully result = preact_iso_url_pattern_match("/user/test%", "/user/:id") # Should either work or return nil, but not crash refute_nil result end def test_malformed_percent_encoding_rest_param # Test malformed percent encoding in rest param - should not crash result = preact_iso_url_pattern_match("/files/test%/file", "/files/:path+") # Should either work or return nil, but not crash refute_nil result end def test_invalid_unicode_sequence # Test invalid unicode sequence - should not crash result = preact_iso_url_pattern_match("/user/test%C3", "/user/:id") # Should either work or return nil, but not crash refute_nil result end end # Run tests if this file is executed directly if __FILE__ == $0 puts "Running Ruby tests for preact-iso-url-pattern..." end ================================================ FILE: polyglot-utils/run_tests.sh ================================================ #!/bin/bash # Preact ISO URL Pattern Matching - Test Runner # Runs tests for all language implementations # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Track test results TOTAL_LANGUAGES=4 PASSED_LANGUAGES=0 echo "========================================" echo "Preact ISO URL Pattern - Test Runner" echo "========================================" echo # Function to run a test and track results run_test() { local language=$1 local directory=$2 local command=$3 local description=$4 echo -e "${BLUE}Testing $language${NC} ($description)" echo "----------------------------------------" # Change to test directory and run command if cd "$directory" 2>/dev/null; then if eval "$command"; then echo -e "${GREEN}$language tests PASSED${NC}" ((PASSED_LANGUAGES++)) else echo -e "${RED}$language tests FAILED${NC}" fi else echo -e "${RED}$language tests FAILED - Directory not found${NC}" fi echo # Return to script directory cd "$SCRIPT_DIR" 2>/dev/null || true } # Get the script directory to ensure we're in the right place SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # Run tests for each language run_test "Go" "go" "go test -v" "Static typing with struct returns" run_test "Python" "python" "python3 test_preact_iso_url_pattern.py" "Dictionary-based with optional typing" run_test "Ruby" "ruby" "ruby test_preact_iso_url_pattern.rb" "Hash-based with flexible syntax" run_test "PHP" "php" "php test_preact_iso_url_pattern.php" "Mixed array/object approach" # Summary echo "========================================" echo "Test Summary" echo "========================================" if [ $PASSED_LANGUAGES -eq $TOTAL_LANGUAGES ]; then echo -e "${GREEN}All $TOTAL_LANGUAGES language implementations passed their tests!${NC}" echo -e "${GREEN}Total tests across all languages: 204 (51 × 4)${NC}" exit 0 else echo -e "${RED}$PASSED_LANGUAGES/$TOTAL_LANGUAGES language implementations passed${NC}" echo -e "${RED}$(($TOTAL_LANGUAGES - $PASSED_LANGUAGES)) language(s) failed${NC}" exit 1 fi ================================================ FILE: src/hydrate.d.ts ================================================ import { ComponentChild, ContainerNode } from 'preact'; export default function hydrate(jsx: ComponentChild, parent?: ContainerNode): void; ================================================ FILE: src/hydrate.js ================================================ import { render, hydrate as hydrativeRender } from 'preact'; let initialized; /** @type {typeof hydrativeRender} */ export default function hydrate(jsx, parent) { if (typeof window === 'undefined') return; let isodata = document.querySelector('script[type=isodata]'); // @ts-ignore-next parent = parent || (isodata && isodata.parentNode) || document.body; if (!initialized && isodata) { hydrativeRender(jsx, parent); } else { render(jsx, parent); } initialized = true; } ================================================ FILE: src/index.d.ts ================================================ export { default as prerender } from './prerender.js'; export * from './router.js'; export { default as lazy, ErrorBoundary } from './lazy.js'; export { default as hydrate } from './hydrate.js'; ================================================ FILE: src/index.js ================================================ export { Router, LocationProvider, useLocation, Route, useRoute } from './router.js'; export { default as lazy, ErrorBoundary } from './lazy.js'; export { default as hydrate } from './hydrate.js'; export function prerender(vnode, options) { return import('./prerender.js').then(m => m.default(vnode, options)); } ================================================ FILE: src/internal.d.ts ================================================ /// import { Component } from 'preact'; export interface AugmentedComponent extends Component { __v: VNode; __c: (error: Promise, suspendingVNode: VNode) => void; } export interface VNode

extends preact.VNode

{ __c: AugmentedComponent; __e?: Element | Text; __u: number; __h: boolean; __v?: VNode

; __k: Array> | null; } export {} ================================================ FILE: src/lazy.d.ts ================================================ import { ComponentChildren, VNode } from 'preact'; export default function lazy(load: () => Promise<{ default: T } | T>): T & { preload: () => Promise; }; export function ErrorBoundary(props: { children?: ComponentChildren; onError?: (error: Error) => void }): VNode; ================================================ FILE: src/lazy.js ================================================ import { h, options } from 'preact'; import { useState, useRef } from 'preact/hooks'; const oldDiff = options.__b; options.__b = (vnode) => { if (vnode.type && vnode.type._forwarded && vnode.ref) { vnode.props.ref = vnode.ref; vnode.ref = null; } if (oldDiff) oldDiff(vnode); }; export default function lazy(load) { let p, c; const loadModule = () => load().then(m => (c = (m && m.default) || m)); const LazyComponent = props => { const [, update] = useState(0); const r = useRef(c); if (!p) p = loadModule(); if (c !== undefined) return h(c, props); if (!r.current) r.current = p.then(() => update(1)); throw p; }; LazyComponent.preload = () => { if (!p) p = loadModule(); return p; } LazyComponent._forwarded = true; return LazyComponent; } // See https://github.com/preactjs/preact/blob/88680e91ec0d5fc29d38554a3e122b10824636b6/compat/src/suspense.js#L5 const oldCatchError = options.__e; options.__e = (err, newVNode, oldVNode) => { if (err && err.then) { let v = newVNode; while ((v = v.__)) { if (v.__c && v.__c.__c) { if (newVNode.__e == null) { newVNode.__c.__z = [oldVNode.__e]; newVNode.__e = oldVNode.__e; // ._dom newVNode.__k = oldVNode.__k; // ._children } if (!newVNode.__k) newVNode.__k = []; return v.__c.__c(err, newVNode); } } } if (oldCatchError) oldCatchError(err, newVNode, oldVNode); }; export function ErrorBoundary(props) { this.__c = childDidSuspend; this.componentDidCatch = props.onError; return props.children; } function childDidSuspend(err) { err.then(() => this.forceUpdate()); } ================================================ FILE: src/prerender.d.ts ================================================ import { VNode } from 'preact'; export interface PrerenderOptions { props?: Record; } export interface PrerenderResult { html: string; links?: Set } export default function prerender( vnode: VNode, options?: PrerenderOptions ): Promise; export function locationStub(path: string): void; ================================================ FILE: src/prerender.js ================================================ import { h, options, cloneElement } from 'preact'; import { renderToStringAsync } from 'preact-render-to-string'; let vnodeHook; const old = options.vnode; options.vnode = vnode => { if (old) old(vnode); if (vnodeHook) vnodeHook(vnode); }; /** * @param {ReturnType} vnode The root JSX element to render (eg: ``) * @param {object} [options] * @param {object} [options.props] Additional props to merge into the root JSX element */ export default async function prerender(vnode, options) { options = options || {}; const props = options.props; if (typeof vnode === 'function') { vnode = h(vnode, props); } else if (props) { vnode = cloneElement(vnode, props); } let links = new Set(); vnodeHook = ({ type, props }) => { if (type === 'a' && props && props.href && (!props.target || props.target === '_self')) { links.add(props.href); } }; try { let html = await renderToStringAsync(vnode); html += ``; return { html, links }; } finally { vnodeHook = null; } } /** * Update `location` to current URL so routers can use things like `location.pathname` * * @param {string} path - current URL path */ export function locationStub(path) { globalThis.location = {}; const u = new URL(path, 'http://localhost'); for (const i in u) { try { globalThis.location[i] = /to[A-Z]/.test(i) ? u[i].bind(u) : String(u[i]); } catch {} } } ================================================ FILE: src/router-navigation-api.d.ts ================================================ import { AnyComponent, ComponentChildren, Context, VNode } from 'preact'; export const LocationProvider: { (props: { scope?: string | RegExp; children?: ComponentChildren; }): VNode; ctx: Context; }; type NestedArray = Array>; interface KnownProps { path: string; query: Record; params: Record; default?: boolean; rest?: string; component?: AnyComponent; } interface ArbitraryProps { [prop: string]: any; } type MatchProps = KnownProps & ArbitraryProps; /** * Check if a URL path matches against a URL path pattern. * * Warning: This is largely an internal API, it may change in the future * @param url - URL path (e.g. /user/12345) * @param route - URL pattern (e.g. /user/:id) */ export function exec(url: string, route: string, matches?: MatchProps): MatchProps export function Router(props: { onRouteChange?: (url: string) => void; onLoadEnd?: (url: string) => void; onLoadStart?: (url: string) => void; children?: NestedArray; }): VNode; interface LocationHook { url: string; path: string; query: Record; } export const useLocation: () => LocationHook; interface RouteHook { path: string; query: Record; params: Record; } export const useRoute: () => RouteHook; type RoutableProps = | { path: string; default?: false; } | { path?: never; default: true; } export type RouteProps = RoutableProps & { component: AnyComponent }; export type RoutePropsForPath = Path extends '*' ? { params: {}; rest: string } : Path extends `:${infer placeholder}?/${infer rest}` ? { [k in placeholder]?: string } & { params: RoutePropsForPath['params'] & { [k in placeholder]?: string } } & Omit, 'params'> : Path extends `:${infer placeholder}/${infer rest}` ? { [k in placeholder]: string } & { params: RoutePropsForPath['params'] & { [k in placeholder]: string } } & Omit, 'params'> : Path extends `:${infer placeholder}?` ? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } } : Path extends `:${infer placeholder}*` ? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } } : Path extends `:${infer placeholder}+` ? { [k in placeholder]: string } & { params: { [k in placeholder]: string } } : Path extends `:${infer placeholder}` ? { [k in placeholder]: string } & { params: { [k in placeholder]: string } } : Path extends (`/${infer rest}` | `${infer _}/${infer rest}`) ? RoutePropsForPath : { params: {} }; export function Route(props: RouteProps & Partial): VNode; declare module 'preact' { // The code below automatically adds `path` and `default` as optional props for every component // (effectively reserving those names, so no component should use those names in its own props). // These declarations extend from `RouteableProps`, which is not allowed in modern TypeScript and // causes a TS2312 error. However, the compiler does seems to honor the intent of this code, so // to avoid an API regression, let's ignore the error rather than loosening the type validation. namespace JSX { /** @ts-ignore */ interface IntrinsicAttributes extends RoutableProps {} } /** @ts-ignore */ interface Attributes extends RoutableProps {} } ================================================ FILE: src/router-navigation-api.js ================================================ import { h, createContext, cloneElement, toChildArray } from 'preact'; import { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact/hooks'; /** * @template T * @typedef {import('preact').RefObject} RefObject * @typedef {import('./internal.d.ts').VNode} VNode */ /** * @param {NavigateEvent} e */ function isSameWindow(e) { const sourceElement = /** @type {HTMLAnchorElement | null} */ (e.sourceElement); return ( !sourceElement || !sourceElement.target || /^(_self)?$/i.test(sourceElement.target) ); } /** @type {string | RegExp | undefined} */ let scope; /** * @param {URL} url * @returns {boolean} */ function isInScope(url) { return !scope || (typeof scope == 'string' ? url.pathname.startsWith(scope) : scope.test(url.pathname) ); } /** * @param {string} state * @param {NavigateEvent} e */ function handleNav(state, e) { const url = new URL(e.destination.url); if ( !e.canIntercept || e.hashChange || e.downloadRequest !== null || !isSameWindow(e) || !isInScope(url) ) { // This is set purely for our test suite so that we can check // if the event was ignored in another `navigate` handler. e['preact-iso-ignored'] = true; return state; } e.intercept(); return url.href.replace(url.origin, ''); } export const exec = (url, route, matches = {}) => { url = url.split('/').filter(Boolean); route = (route || '').split('/').filter(Boolean); if (!matches.params) matches.params = {}; for (let i = 0, val, rest; i < Math.max(url.length, route.length); i++) { let [, m, param, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/); val = url[i]; // segment match: if (!m && param == val) continue; // /foo/* match if (!m && val && flag == '*') { matches.rest = '/' + url.slice(i).map(decodeURIComponent).join('/'); break; } // segment mismatch / missing required field: if (!m || (!val && flag != '?' && flag != '*')) return; rest = flag == '+' || flag == '*'; // rest (+/*) match: if (rest) val = url.slice(i).join('/') || undefined; // normal/optional field: else if (val) val = decodeURIComponent(val); matches.params[param] = val; if (!(param in matches)) matches[param] = val; if (rest) break; } return matches; }; /** * @param {Object} props * @param {string | RegExp} [props.scope] * @param {import('preact').ComponentChildren} [props.children] */ export function LocationProvider(props) { const [url, route] = useReducer(handleNav, location.pathname + location.search); if (props.scope) scope = props.scope; const value = useMemo(() => { const u = new URL(url, location.origin); const path = u.pathname.replace(/\/+$/g, '') || '/'; return { url, path, query: Object.fromEntries(u.searchParams), }; }, [url]); useLayoutEffect(() => { navigation.addEventListener('navigate', route) return () => { navigation.removeEventListener('navigate', route) }; }, []); return h(LocationProvider.ctx.Provider, { value }, props.children); } const RESOLVED = Promise.resolve(); /** @this {import('./internal.d.ts').AugmentedComponent} */ export function Router(props) { const [c, update] = useReducer(c => c + 1, 0); const { url, query, path } = useLocation(); if (!url) { throw new Error(`preact-iso's must be used within a , see: https://github.com/preactjs/preact-iso#locationprovider`); } const { rest = path, params = {} } = useContext(RouteContext); const isLoading = useRef(false); const prevRoute = useRef(path); // Monotonic counter used to check if an un-suspending route is still the current route: const count = useRef(0); // The current route: const cur = /** @type {RefObject>} */ (useRef()); // Previous route (if current route is suspended): const prev = /** @type {RefObject>} */ (useRef()); // A not-yet-hydrated DOM root to remove once we commit: const pendingBase = /** @type {RefObject} */ (useRef()); // has this component ever successfully rendered without suspending: const hasEverCommitted = useRef(false); // was the most recent render successful (did not suspend): const didSuspend = /** @type {RefObject} */ (useRef()); didSuspend.current = false; let pathRoute, defaultRoute, matchProps; toChildArray(props.children).some((/** @type {VNode} */ vnode) => { const matches = exec( rest, vnode.props.path, (matchProps = { ...vnode.props, path: rest, query, params: Object.assign({}, params), rest: '' }) ); if (matches) return (pathRoute = cloneElement(vnode, matchProps)); if (vnode.props.default) defaultRoute = cloneElement(vnode, matchProps); }); /** @type {VNode | undefined} */ let incoming = pathRoute || defaultRoute; const isHydratingSuspense = cur.current && cur.current.__u & MODE_HYDRATE && cur.current.__u & MODE_SUSPENDED; const isHydratingBool = cur.current && cur.current.__h; const routeChanged = useMemo(() => { prev.current = cur.current; cur.current = /** @type {VNode} */ (h(RouteContext.Provider, { value: matchProps }, incoming)); // Only mark as an update if the route component changed. const outgoing = prev.current && prev.current.props.children; if (!outgoing || !incoming || incoming.type !== outgoing.type || incoming.props.component !== outgoing.props.component) { // This hack prevents Preact from diffing when we swap `cur` to `prev`: if (this.__v && this.__v.__k) this.__v.__k.reverse(); count.current++; return true; } return false; }, [url, JSON.stringify(matchProps)]); if (isHydratingSuspense) { cur.current.__u |= MODE_HYDRATE; cur.current.__u |= MODE_SUSPENDED; } else if (isHydratingBool) { cur.current.__h = true; } // Reset previous children - if rendering succeeds synchronously, we shouldn't render the previous children. const p = prev.current; prev.current = null; // This borrows the _childDidSuspend() solution from compat. this.__c = (e, suspendedVNode) => { // Mark the current render as having suspended: didSuspend.current = true; // The new route suspended, so keep the previous route around while it loads: prev.current = p; // Fire an event saying we're waiting for the route: if (props.onLoadStart) props.onLoadStart(url); isLoading.current = true; // Re-render on unsuspend: let c = count.current; e.then(() => { // Ignore this update if it isn't the most recently suspended update: if (c !== count.current) return; // Successful route transition: un-suspend after a tick and stop rendering the old route: prev.current = null; if (cur.current) { if (suspendedVNode.__h) { // _hydrating cur.current.__h = suspendedVNode.__h; } if (suspendedVNode.__u & MODE_SUSPENDED) { // _flags cur.current.__u |= MODE_SUSPENDED; } if (suspendedVNode.__u & MODE_HYDRATE) { cur.current.__u |= MODE_HYDRATE; } } RESOLVED.then(update); }); }; useLayoutEffect(() => { const currentDom = this.__v && this.__v.__e; // Ignore suspended renders (failed commits): if (didSuspend.current) { // If we've never committed, mark any hydration DOM for removal on the next commit: if (!hasEverCommitted.current && !pendingBase.current) { pendingBase.current = currentDom; } return; } // If this is the first ever successful commit and we didn't use the hydration DOM, remove it: if (!hasEverCommitted.current && pendingBase.current) { if (pendingBase.current !== currentDom) pendingBase.current.remove(); pendingBase.current = null; } // Mark the component has having committed: hasEverCommitted.current = true; // The route is loaded and rendered. if (prevRoute.current !== path) { if (props.onRouteChange) props.onRouteChange(url); prevRoute.current = path; } if (props.onLoadEnd && isLoading.current) props.onLoadEnd(url); isLoading.current = false; }, [path, c]); // Note: cur MUST render first in order to set didSuspend & prev. return routeChanged ? [h(RenderRef, { r: cur }), h(RenderRef, { r: prev })] : h(RenderRef, { r: cur }); } const MODE_HYDRATE = 1 << 5; const MODE_SUSPENDED = 1 << 7; // Lazily render a ref's current value: const RenderRef = ({ r }) => r.current; Router.Provider = LocationProvider; LocationProvider.ctx = createContext( /** @type {import('./router-navigation-api.d.ts').LocationHook} */ ({}) ); const RouteContext = createContext( /** @type {import('./router-navigation-api.d.ts').RouteHook & { rest: string }} */ ({}) ); export const Route = props => h(props.component, props); export const useLocation = () => useContext(LocationProvider.ctx); export const useRoute = () => useContext(RouteContext); ================================================ FILE: src/router.d.ts ================================================ import { AnyComponent, ComponentChildren, Context, VNode } from 'preact'; export const LocationProvider: { (props: { scope?: string | RegExp; children?: ComponentChildren; }): VNode; ctx: Context; }; type NestedArray = Array>; interface KnownProps { path: string; query: Record; params: Record; default?: boolean; rest?: string; component?: AnyComponent; } interface ArbitraryProps { [prop: string]: any; } type MatchProps = KnownProps & ArbitraryProps; /** * Check if a URL path matches against a URL path pattern. * * Warning: This is largely an internal API, it may change in the future * @param url - URL path (e.g. /user/12345) * @param route - URL pattern (e.g. /user/:id) */ export function exec(url: string, route: string, matches?: MatchProps): MatchProps export function Router(props: { onRouteChange?: (url: string) => void; onLoadEnd?: (url: string) => void; onLoadStart?: (url: string) => void; children?: NestedArray; }): VNode; interface LocationHook { url: string; path: string; query: Record; route: (url: string, replace?: boolean) => void; back: () => void; forward: () => void; } export const useLocation: () => LocationHook; interface RouteHook { path: string; query: Record; params: Record; } export const useRoute: () => RouteHook; type RoutableProps = | { path: string; default?: false; } | { path?: never; default: true; } export type RouteProps = RoutableProps & { component: AnyComponent }; export type RoutePropsForPath = Path extends '*' ? { params: {}; rest: string } : Path extends `:${infer placeholder}?/${infer rest}` ? { [k in placeholder]?: string } & { params: RoutePropsForPath['params'] & { [k in placeholder]?: string } } & Omit, 'params'> : Path extends `:${infer placeholder}/${infer rest}` ? { [k in placeholder]: string } & { params: RoutePropsForPath['params'] & { [k in placeholder]: string } } & Omit, 'params'> : Path extends `:${infer placeholder}?` ? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } } : Path extends `:${infer placeholder}*` ? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } } : Path extends `:${infer placeholder}+` ? { [k in placeholder]: string } & { params: { [k in placeholder]: string } } : Path extends `:${infer placeholder}` ? { [k in placeholder]: string } & { params: { [k in placeholder]: string } } : Path extends (`/${infer rest}` | `${infer _}/${infer rest}`) ? RoutePropsForPath : { params: {} }; export function Route(props: RouteProps & Partial): VNode; declare module 'preact' { // The code below automatically adds `path` and `default` as optional props for every component // (effectively reserving those names, so no component should use those names in its own props). // These declarations extend from `RouteableProps`, which is not allowed in modern TypeScript and // causes a TS2312 error. However, the compiler does seems to honor the intent of this code, so // to avoid an API regression, let's ignore the error rather than loosening the type validation. namespace JSX { /** @ts-ignore */ interface IntrinsicAttributes extends RoutableProps {} } /** @ts-ignore */ interface Attributes extends RoutableProps {} } ================================================ FILE: src/router.js ================================================ import { h, createContext, cloneElement, toChildArray } from 'preact'; import { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact/hooks'; /** * @template T * @typedef {import('preact').RefObject} RefObject * @typedef {import('./internal.d.ts').VNode} VNode */ /** @type {boolean} */ let push; /** @type {string | RegExp | undefined} */ let scope; /** * @param {string} href * @returns {boolean} */ function isInScope(href) { return !scope || (typeof scope == 'string' ? href.startsWith(scope) : scope.test(href) ); } /** * @param {string} state * @param {MouseEvent | PopStateEvent | { url: string, replace?: boolean }} action */ function handleNav(state, action) { let url = ''; push = undefined; if (action && action.type === 'click') { // ignore events the browser takes care of already: if (action.ctrlKey || action.metaKey || action.altKey || action.shiftKey || action.button !== 0) { return state; } const link = action.composedPath().find(el => el.nodeName == 'A' && el.href), href = link && link.getAttribute('href'); if ( !link || link.origin != location.origin || /^#/.test(href) || !/^(_?self)?$/i.test(link.target) || !isInScope(href) || link.download ) { return state; } push = true; action.preventDefault(); url = link.href.replace(location.origin, ''); } else if (action && action.url) { push = !action.replace; url = action.url; } else { url = location.pathname + location.search; } if (push === true) history.pushState(null, '', url); else if (push === false) history.replaceState(null, '', url); return url; }; export const exec = (url, route, matches = {}) => { url = url.split('/').filter(Boolean); route = (route || '').split('/').filter(Boolean); if (!matches.params) matches.params = {}; for (let i = 0, val, rest; i < Math.max(url.length, route.length); i++) { let [, m, param, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/); val = url[i]; // segment match: if (!m && param == val) continue; // /foo/* match if (!m && val && flag == '*') { matches.rest = '/' + url.slice(i).join('/'); break; } // segment mismatch / missing required field: if (!m || (!val && flag != '?' && flag != '*')) return; rest = flag == '+' || flag == '*'; // rest (+/*) match: if (rest) val = url.slice(i).map(decodeURIComponent).join('/') || undefined; // normal/optional field: else if (val) val = decodeURIComponent(val); matches.params[param] = val; if (!(param in matches)) matches[param] = val; if (rest) break; } return matches; }; /** * @param {Object} props * @param {string | RegExp} [props.scope] * @param {import('preact').ComponentChildren} [props.children] */ export function LocationProvider(props) { // @ts-expect-error - props.url is not implemented correctly & will be removed in the future const [url, route] = useReducer(handleNav, props.url || location.pathname + location.search); if (props.scope) scope = props.scope; const wasPush = push === true; const value = useMemo(() => { const u = new URL(url, location.origin); const path = u.pathname.replace(/\/+$/g, '') || '/'; // @ts-ignore-next return { url, path, query: Object.fromEntries(u.searchParams), route: (url, replace) => route({ url, replace }), back: () => { history.back(); }, forward: () => { history.forward(); }, wasPush }; }, [url]); useLayoutEffect(() => { addEventListener('click', route); addEventListener('popstate', route); return () => { removeEventListener('click', route); removeEventListener('popstate', route); }; }, []); // @ts-ignore return h(LocationProvider.ctx.Provider, { value }, props.children); } const RESOLVED = Promise.resolve(); /** @this {import('./internal.d.ts').AugmentedComponent} */ export function Router(props) { const [c, update] = useReducer(c => c + 1, 0); const { url, query, wasPush, path } = useLocation(); if (!url) { throw new Error(`preact-iso's must be used within a , see: https://github.com/preactjs/preact-iso#locationprovider`); } const { rest = path, params = {} } = useContext(RouteContext); const isLoading = useRef(false); const prevRoute = useRef(path); // Monotonic counter used to check if an un-suspending route is still the current route: const count = useRef(0); // The current route: const cur = /** @type {RefObject>} */ (useRef()); // Previous route (if current route is suspended): const prev = /** @type {RefObject>} */ (useRef()); // A not-yet-hydrated DOM root to remove once we commit: const pendingBase = /** @type {RefObject} */ (useRef()); // has this component ever successfully rendered without suspending: const hasEverCommitted = useRef(false); // was the most recent render successful (did not suspend): const didSuspend = /** @type {RefObject} */ (useRef()); didSuspend.current = false; let pathRoute, defaultRoute, matchProps; toChildArray(props.children).some((/** @type {VNode} */ vnode) => { const matches = exec( rest, vnode.props.path, (matchProps = { ...vnode.props, path: rest, query, params: Object.assign({}, params), rest: '' }) ); if (matches) return (pathRoute = cloneElement(vnode, matchProps)); if (vnode.props.default) defaultRoute = cloneElement(vnode, matchProps); }); /** @type {VNode | undefined} */ let incoming = pathRoute || defaultRoute; const isHydratingSuspense = cur.current && cur.current.__u & MODE_HYDRATE && cur.current.__u & MODE_SUSPENDED; const isHydratingBool = cur.current && cur.current.__h; const routeChanged = useMemo(() => { prev.current = cur.current; cur.current = /** @type {VNode} */ (h(RouteContext.Provider, { value: matchProps }, incoming)); // Only mark as an update if the route component changed. const outgoing = prev.current && prev.current.props.children; if (!outgoing || !incoming || incoming.type !== outgoing.type || incoming.props.component !== outgoing.props.component) { // This hack prevents Preact from diffing when we swap `cur` to `prev`: if (this.__v && this.__v.__k) this.__v.__k.reverse(); count.current++; return true; } return false; }, [url, JSON.stringify(matchProps)]); if (isHydratingSuspense) { cur.current.__u |= MODE_HYDRATE; cur.current.__u |= MODE_SUSPENDED; } else if (isHydratingBool) { cur.current.__h = true; } // Reset previous children - if rendering succeeds synchronously, we shouldn't render the previous children. const p = prev.current; prev.current = null; // This borrows the _childDidSuspend() solution from compat. this.__c = (e, suspendedVNode) => { // Mark the current render as having suspended: didSuspend.current = true; // The new route suspended, so keep the previous route around while it loads: prev.current = p; // Fire an event saying we're waiting for the route: if (props.onLoadStart) props.onLoadStart(url); isLoading.current = true; // Re-render on unsuspend: let c = count.current; e.then(() => { // Ignore this update if it isn't the most recently suspended update: if (c !== count.current) return; // Successful route transition: un-suspend after a tick and stop rendering the old route: prev.current = null; if (cur.current) { if (suspendedVNode.__h) { // _hydrating cur.current.__h = suspendedVNode.__h; } if (suspendedVNode.__u & MODE_SUSPENDED) { // _flags cur.current.__u |= MODE_SUSPENDED; } if (suspendedVNode.__u & MODE_HYDRATE) { cur.current.__u |= MODE_HYDRATE; } } RESOLVED.then(update); }); }; useLayoutEffect(() => { const currentDom = this.__v && this.__v.__e; // Ignore suspended renders (failed commits): if (didSuspend.current) { // If we've never committed, mark any hydration DOM for removal on the next commit: if (!hasEverCommitted.current && !pendingBase.current) { pendingBase.current = currentDom; } return; } // If this is the first ever successful commit and we didn't use the hydration DOM, remove it: if (!hasEverCommitted.current && pendingBase.current) { if (pendingBase.current !== currentDom) pendingBase.current.remove(); pendingBase.current = null; } // Mark the component has having committed: hasEverCommitted.current = true; // The route is loaded and rendered. if (prevRoute.current !== path) { if (wasPush) scrollTo(0, 0); if (props.onRouteChange) props.onRouteChange(url); prevRoute.current = path; } if (props.onLoadEnd && isLoading.current) props.onLoadEnd(url); isLoading.current = false; }, [path, wasPush, c]); // Note: cur MUST render first in order to set didSuspend & prev. return routeChanged ? [h(RenderRef, { r: cur }), h(RenderRef, { r: prev })] : h(RenderRef, { r: cur }); } const MODE_HYDRATE = 1 << 5; const MODE_SUSPENDED = 1 << 7; // Lazily render a ref's current value: const RenderRef = ({ r }) => r.current; Router.Provider = LocationProvider; LocationProvider.ctx = createContext( /** @type {import('./router.d.ts').LocationHook & { wasPush: boolean }} */ ({}) ); const RouteContext = createContext( /** @type {import('./router.d.ts').RouteHook & { rest: string }} */ ({}) ); export const Route = props => h(props.component, props); export const useLocation = () => useContext(LocationProvider.ctx); export const useRoute = () => useContext(RouteContext); ================================================ FILE: test/lazy.test.js ================================================ import { h, render } from 'preact'; import * as chai from 'chai'; import * as sinon from 'sinon'; import sinonChai from 'sinon-chai'; import { LocationProvider, Router } from '../src/router.js'; import lazy, { ErrorBoundary } from '../src/lazy.js'; import './setup.js'; const expect = chai.expect; chai.use(sinonChai); describe('lazy', () => { let scratch; beforeEach(() => { if (scratch) { render(null, scratch); scratch.remove(); } scratch = document.createElement('scratch'); document.body.appendChild(scratch); history.replaceState(null, null, '/'); }); it('should support preloading lazy imports', async () => { const A = () =>

A

; const loadB = sinon.fake(() => Promise.resolve(() =>

B

)); const B = lazy(loadB); render( , scratch ); expect(loadB).not.to.have.been.called; await B.preload(); expect(loadB).to.have.been.calledOnce; }); it('should forward refs', async () => { const A = (props) =>

A

; const LazyA = lazy(() => Promise.resolve(A)); const ref = {}; render( , scratch ); await new Promise(r => setTimeout(r, 1)) if (ref.current.constructor === A) { // v10 expect(ref.current.constructor).to.equal(A); } else { // v11+ expect(ref.current).to.equal(scratch.firstChild); } }); }); ================================================ FILE: test/node/location-stub.test.js ================================================ import { test } from 'uvu'; import * as assert from 'uvu/assert'; import { locationStub } from '../../src/prerender.js'; test.before.each(() => { if (globalThis.location) { delete globalThis.location; } }); test('Contains all Location instance properties', () => { locationStub('/foo/bar?baz=qux#quux'); [ // 'ancestorOrigins', // Not supported by FireFox and sees little use, but we could add an empty val if it's needed 'hash', 'host', 'hostname', 'href', 'origin', 'pathname', 'port', 'protocol', 'search', ].forEach(key => { assert.ok(Object.hasOwn(globalThis.location, key), `missing: ${key}`); }); }); // Do we need to support `assign`, `reload`, and/or `replace`? test('Support bound methods', () => { locationStub('/foo/bar?baz=qux#quux'); assert.equal(globalThis.location.toString(), 'http://localhost/foo/bar?baz=qux#quux') }); test.run(); ================================================ FILE: test/node/pattern-match.types.ts ================================================ // Test this file by running: // npx tsc --noEmit test/node/pattern-match.types.ts import type { RoutePropsForPath } from '../../src/router.js'; // Test utils type isEqualsType = T extends U ? U extends T ? true : false : false; type isWeakEqualsType = T extends U ? true : false; // Type tests based on router-match.test.js cases // Base route test const test1: isEqualsType< RoutePropsForPath<'/'> , { params: {} } > = true; const test1_1: isEqualsType< RoutePropsForPath<'/'> , { arbitrary: {} } > = false; // Param route test const test2: isEqualsType< RoutePropsForPath<'/user/:id'> , { params: { id: string }, id: string } > = true; const test2_weak: isWeakEqualsType< RoutePropsForPath<'/user/:id'> , { params: { id: string } } > = true; // Param rest segment test const test3: isEqualsType< RoutePropsForPath<'/user/*'> , { params: {}, rest: string } > = true; const test3_1: isEqualsType< RoutePropsForPath<'/*'> , { params: {}, rest: string } > = true; const test3_2: isEqualsType< RoutePropsForPath<'*'> , { params: {}, rest: string } > = true; // Param route with rest segment test const test4: isEqualsType< RoutePropsForPath<'/user/:id/*'> , { params: { id: string }, id: string, rest: string } > = true; // Optional param route test const test5: isEqualsType< RoutePropsForPath<'/user/:id?'> , { params: { id?: string }, id?: string } > = true; // Optional rest param route "/:x*" test const test6: isEqualsType< RoutePropsForPath<'/user/:id*'> , { params: { id?: string }, id?: string } > = true; // rest param should not be present const test6_error: isEqualsType< RoutePropsForPath<'/user/:id*'> , { params: { id: string }, rest: string } > = false; // Rest param route "/:x+" test const test7: isEqualsType< RoutePropsForPath<'/user/:id+'> , { params: { id: string }, id: string } > = true; // rest param should not be present const test7_error: isEqualsType< RoutePropsForPath<'/user/:id+'>, { params: { id: string }, id: string, rest: string } > = false; // Handles leading/trailing slashes test const test8: isEqualsType< RoutePropsForPath<'/about-late/:seg1/:seg2/'> , { params: { seg1: string; seg2: string }, seg1: string, seg2: string } > = true; // Multiple params test (from overwrite properties test) const test9: isEqualsType< RoutePropsForPath<'/:path/:query'> , { params: { path: string; query: string }, path: string, query: string } > = true; // Empty route test const test10: isEqualsType< RoutePropsForPath<''> , { params: {} } > = true; ================================================ FILE: test/node/prerender.test.js ================================================ import { test } from 'uvu'; import * as assert from 'uvu/assert'; import { html } from 'htm/preact'; import { default as prerender } from '../../src/prerender.js'; test('extracts links', async () => { const App = () => html`
`; const { links } = await prerender(html`<${App} />`); assert.equal(links.size, 2, `incorrect number of links found: ${links.size}`); assert.ok(links.has('/foo'), `missing: /foo`); assert.ok(links.has('/baz'), `missing: /baz`); }); test('appends iso data script', async () => { const { html: h } = await prerender(html`
`); // Empty for now, but used for hydration vs render detection assert.match(h, /