-
-
Notifications
You must be signed in to change notification settings - Fork 17
Implemented router's URL matching logic in multiple (non-JS) languages #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6b8f51a
implemented router' URL matching logic in multiple (non-JS) languages…
Munawwar 113c5e8
fix go lang bug where "/user" matched "/user/:id?"
Munawwar 8fe999f
removed silly tests, added few, synced all 4 test suites
Munawwar a1be900
review
Munawwar 2407e63
docs: Slightly adjust position/wording in root readme
rschristian 47eebd8
chore: Clean up trailing whitespace
rschristian File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Language-specific files to ignore | ||
| __pycache__/ | ||
| *.pyc | ||
| .venv/ | ||
| .ruby-version |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| # 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 `<link rel="preload">` 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: | ||
|
|
||
| ### 1. Route Configuration (routes.json) | ||
| ```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) | ||
Munawwar marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ```json | ||
| { | ||
| "pages/UserPosts.jsx": { | ||
| "file": "assets/UserPosts-abc123.js", | ||
| "css": ["assets/UserPosts-def456.css"], | ||
| "imports": ["chunks/shared-ghi789.js"] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 3. Server Implementation | ||
Munawwar marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ```python | ||
| # Python example | ||
| import json | ||
|
|
||
| routes = json.load(open('routes.json')) | ||
| manifest = json.load(open('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'<link rel="modulepreload" crossorigin href="{js_file}">') | ||
|
|
||
| for css_file in entry_info.get('css', []): | ||
| preload_tags.append(f'<link rel="stylesheet" crossorigin href="{css_file}">') | ||
| # 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 | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # 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"}` | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| module myproject | ||
|
|
||
| go 1.13 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| // 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")) | ||
| // } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.