@b9g/match-pattern 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # MatchPattern
2
2
 
3
- Extended URLPattern for better routing with enhanced search parameter handling.
3
+ High-performance URLPattern-compatible implementation for web routing with enhanced search parameter handling.
4
4
 
5
5
  ## Overview
6
6
 
7
- MatchPattern is a subclass of URLPattern that fixes its limitations for web routing while maintaining full backward compatibility. It enhances URLPattern with order-independent search parameters, unified parameter extraction, and convenient string pattern syntax.
7
+ This package provides two classes:
8
+
9
+ - **URLPattern**: A 100% WPT-compliant implementation that's ~60x faster than the polyfill/native
10
+ - **MatchPattern**: Same performance with routing enhancements (order-independent search params, unified `params` object)
11
+
12
+ Both compile patterns directly to RegExp in a single pass, bypassing the multi-stage pipeline used by polyfill/native implementations.
8
13
 
9
14
  ## Installation
10
15
 
@@ -15,19 +20,45 @@ npm install @b9g/match-pattern
15
20
  ## Basic Usage
16
21
 
17
22
  ```javascript
18
- import { MatchPattern } from '@b9g/match-pattern';
23
+ import { MatchPattern, URLPattern } from '@b9g/match-pattern';
19
24
 
20
- // Create patterns with enhanced string syntax
25
+ // URLPattern: 100% WPT-compliant, ~60x faster than polyfill/native
26
+ const strict = new URLPattern({ pathname: '/api/posts/:id' });
27
+
28
+ // MatchPattern: Same performance + order-independent search params
21
29
  const pattern = new MatchPattern('/api/posts/:id&format=:format');
22
30
  const url = new URL('http://example.com/api/posts/123?format=json&page=1');
23
31
 
24
32
  if (pattern.test(url)) {
25
33
  const result = pattern.exec(url);
26
- console.log(result.params);
34
+ console.log(result.params);
27
35
  // { id: '123', format: 'json', page: '1' }
28
36
  }
29
37
  ```
30
38
 
39
+ ## Performance
40
+
41
+ MatchPattern compiles patterns directly to optimized RegExp in a single pass, while the URLPattern polyfill uses a multi-stage pipeline (lexer → parser → RegExp generator). This results in **~40-60x faster** pattern matching:
42
+
43
+ | Benchmark | URLPattern | MatchPattern | Polyfill | Native |
44
+ |-----------|------------|--------------|----------|--------|
45
+ | Static test() | 37ns | 72ns | 3.02µs | 2.32µs |
46
+ | Dynamic exec() | 304ns | 483ns | 2.45µs | 2.42µs |
47
+ | Construction | 760ns | 634ns | 16.58µs | 16.17µs |
48
+
49
+ *Benchmarks run on Apple M1, Bun 1.3.3. See `bench/urlpattern.bench.js`.*
50
+
51
+ MatchPattern adds ~35ns overhead for order-independent search parameter matching - a feature the [URLPattern spec explicitly doesn't support](https://github.com/whatwg/urlpattern/discussions/60).
52
+
53
+ All URLPattern syntax is fully supported including:
54
+ - Named parameters with regex constraints: `:id(\d+)`
55
+ - Optional parameters: `:id?`
56
+ - Repeat modifiers: `:path+`, `:path*`
57
+ - Wildcards: `*`
58
+ - Regex groups: `(\d+)`
59
+ - Explicit delimiters: `{/old}?`
60
+ - Escaped characters: `\.`
61
+
31
62
  ## Key Differences from URLPattern
32
63
 
33
64
  ### 1. Order-Independent Search Parameters
@@ -39,21 +70,32 @@ const pattern = new MatchPattern({ search: 'type=:type&sort=:sort' });
39
70
 
40
71
  // URLPattern: Only first URL matches
41
72
  // MatchPattern: Both URLs match
42
- pattern.test('/?type=blog&sort=date'); // Both: true
43
- pattern.test('/?sort=date&type=blog'); // MatchPattern: true, URLPattern: false
73
+ pattern.test('/?type=blog&sort=date'); // Both: true
74
+ pattern.test('/?sort=date&type=blog'); // MatchPattern: true, URLPattern: false
44
75
  ```
45
76
 
46
77
  ### 2. Non-Exhaustive Search Matching
47
78
 
48
- URLPattern rejects extra parameters. MatchPattern captures them:
79
+ URLPattern uses greedy capture that lumps extra params into the last parameter value. MatchPattern properly parses them:
49
80
 
50
81
  ```javascript
51
82
  const pattern = new MatchPattern({ search: 'q=:query' });
83
+
84
+ // URLPattern greedy capture issue
85
+ const urlPattern = new URLPattern({ search: 'q=:query' });
86
+ urlPattern.exec('?q=hello&page=1').search.groups; // { query: "hello&page=1" }
87
+
88
+ // MatchPattern proper parsing
52
89
  const result = pattern.exec('/?q=hello&page=1&limit=10');
90
+ console.log(result.params); // { q: 'hello', page: '1', limit: '10' }
91
+ ```
92
+
93
+ Required parameters must be present, but extra parameters are allowed:
53
94
 
54
- // URLPattern: Fails or captures 'hello&page=1&limit=10' as query value
55
- // MatchPattern: { q: 'hello', page: '1', limit: '10' }
56
- console.log(result.params);
95
+ ```javascript
96
+ pattern.test('/search'); // false (q missing)
97
+ pattern.test('/search?q=hello'); // true
98
+ pattern.test('/search?q=hello&page=1&limit=10'); // true (extras captured)
57
99
  ```
58
100
 
59
101
  ### 3. Unified Parameter Object
@@ -77,7 +119,7 @@ MatchPattern supports convenient string patterns with `&` separator:
77
119
  // Pathname only
78
120
  new MatchPattern('/api/posts/:id')
79
121
 
80
- // Pathname with search parameters
122
+ // Pathname with search parameters
81
123
  new MatchPattern('/api/posts/:id&format=:format&page=:page')
82
124
 
83
125
  // Search parameters only
@@ -87,82 +129,123 @@ new MatchPattern('&q=:query&sort=:sort')
87
129
  new MatchPattern('https://api.example.com/v1/posts/:id&format=:format')
88
130
 
89
131
  // Object syntax (same as URLPattern, enhanced behavior)
90
- new MatchPattern({
132
+ new MatchPattern({
91
133
  pathname: '/api/posts/:id',
92
- search: 'format=:format'
134
+ search: 'format=:format'
93
135
  })
94
136
  ```
95
137
 
96
- ## Trailing Slash Normalization
138
+ It's not possible to separate pathname from search with `?` because the syntax is used to indicate optionality.
97
139
 
98
- URLPattern has inconsistent behavior with trailing slashes that can cause unexpected matches.
140
+ ## Trailing Slash Handling
99
141
 
100
- **Issue:** [kenchris/urlpattern-polyfill#131](https://github.com/kenchris/urlpattern-polyfill/issues/131)
101
-
102
- **Solution:** ✅ Automatic trailing slash normalization implemented
142
+ MatchPattern does not automatically normalize trailing slashes. Use explicit patterns:
103
143
 
104
144
  ```javascript
105
- const pattern = new MatchPattern('/api/posts/:id');
106
-
107
- // Both match consistently
108
- pattern.test('/api/posts/123'); // ✅ true
109
- pattern.test('/api/posts/123/'); // ✅ true (normalized)
145
+ // Exact matching
146
+ const exactPattern = new MatchPattern('/api/posts/:id');
147
+ exactPattern.test('/api/posts/123'); // true
148
+ exactPattern.test('/api/posts/123/'); // false
149
+
150
+ // Optional trailing slash
151
+ const flexiblePattern = new MatchPattern('/api/posts/:id{/}?');
152
+ flexiblePattern.test('/api/posts/123'); // true
153
+ flexiblePattern.test('/api/posts/123/'); // true
110
154
  ```
111
155
 
112
- ## Known Limitations
156
+ ## Implementation Notes
157
+
158
+ ### Direct RegExp Compilation
159
+
160
+ MatchPattern compiles URLPattern syntax directly to RegExp in a single pass, while the URLPattern polyfill uses a multi-stage pipeline (lexer → parser → RegExp generator). This approach provides:
161
+ - **Performance**: ~40-60x faster than the URLPattern polyfill and native implementations
162
+ - **Consistency**: Same behavior across all JavaScript runtimes
163
+ - **Zero dependencies**: No polyfill required
164
+ - **Simplicity**: Direct pattern-to-RegExp compilation with minimal overhead
113
165
 
114
- ### Cross-Implementation Differences
166
+ ### URLPattern Spec Compliance
115
167
 
116
- The polyfill and native browser implementations can return different results for edge cases.
168
+ The `URLPattern` class passes 100% of the Web Platform Tests (755 tests). It implements the full URLPattern specification:
117
169
 
118
- **Issue:** [kenchris/urlpattern-polyfill#129](https://github.com/kenchris/urlpattern-polyfill/issues/129)
170
+ - Named parameters: `:id`, `:id(\d+)`
171
+ - Optional parameters: `:id?`
172
+ - Repeat modifiers: `:path+`, `:path*`
173
+ - Wildcards: `*`
174
+ - Regex groups: `(\d+)`
175
+ - Explicit delimiters: `{/old}?`
176
+ - Escaped characters: `\.`
177
+ - Protocol, hostname, port, pathname, search, and hash matching
178
+ - baseURL parameter for relative pattern resolution
179
+ - ignoreCase option
119
180
 
120
- **Impact:** Results may vary between Node.js, browsers, and other runtimes.
181
+ `MatchPattern` intentionally deviates from strict spec compliance in two areas to provide better routing ergonomics:
182
+ - Allows relative patterns without baseURL (convenience for routing)
183
+ - Order-independent search parameter matching
121
184
 
122
- **Testing:** Use Playwright for cross-browser validation in production applications.
185
+ ## Exports
123
186
 
124
- ### TypeScript Compatibility
187
+ ### Classes
188
+
189
+ - `URLPattern` - 100% WPT-compliant URLPattern implementation
190
+ - `MatchPattern` - URLPattern with routing enhancements (order-independent search params, unified params)
191
+
192
+ ### Types
125
193
 
126
- Node.js and polyfill URLPattern types have slight differences in their TypeScript definitions.
194
+ - `MatchPatternResult` - Result type for MatchPattern.exec()
195
+ - `URLPatternOptions` - Options for URLPattern constructor (ignoreCase, etc.)
196
+ - `CompiledPattern` - Internal compiled pattern representation
197
+ - `ParsedPattern` - Parsed pattern structure
198
+ - `PatternSegment` - Individual segment of a parsed pattern
127
199
 
128
- **Issue:** [kenchris/urlpattern-polyfill#135](https://github.com/kenchris/urlpattern-polyfill/issues/135)
200
+ ### Utility Functions
129
201
 
130
- **Solution:** MatchPattern uses canonical WHATWG specification types internally.
202
+ - `isSimplePattern(pathname: string): boolean` - Check if pathname is a simple pattern (no regex features)
203
+ - `parseSimplePattern(pathname: string): ParsedPattern | null` - Parse a simple pattern into segments
204
+ - `compilePathname(pathname: string, options?: object): CompiledPattern` - Compile a pathname pattern to RegExp
131
205
 
132
206
  ## API Reference
133
207
 
134
- ### Constructor
208
+ ### URLPattern
135
209
 
136
210
  ```typescript
137
- new MatchPattern(input: string | URLPatternInit, baseURL?: string)
211
+ new URLPattern(input?: string | URLPatternInit, baseURL?: string, options?: URLPatternOptions)
138
212
  ```
139
213
 
140
- ### Methods
214
+ Methods:
215
+ - `test(input: string | URL | URLPatternInit, baseURL?: string): boolean`
216
+ - `exec(input: string | URL | URLPatternInit, baseURL?: string): URLPatternResult | null`
217
+
218
+ ### MatchPattern
141
219
 
142
220
  ```typescript
143
- // Enhanced methods with unified params
144
- test(input: string | URL): boolean
145
- exec(input: string | URL): MatchPatternResult | null
221
+ new MatchPattern(input: string | URLPatternInit, baseURL?: string)
146
222
  ```
147
223
 
224
+ Methods:
225
+ - `test(input: string | URL): boolean`
226
+ - `exec(input: string | URL): MatchPatternResult | null`
227
+
148
228
  ### Types
149
229
 
150
230
  ```typescript
151
231
  interface MatchPatternResult extends URLPatternResult {
152
232
  params: Record<string, string>; // Unified parameters from all sources
153
233
  }
234
+
235
+ interface URLPatternOptions {
236
+ ignoreCase?: boolean; // Case-insensitive matching
237
+ }
154
238
  ```
155
239
 
156
240
  ## Compatibility
157
241
 
158
- - **Node.js**: 18+ (with URLPattern support) or any version with polyfill
159
- - **Browsers**: Chrome 95+, or any browser with polyfill
160
- - **Runtimes**: Deno, Bun, Cloudflare Workers, Edge Runtime
242
+ - **Runtimes**: Node, Deno, Bun, Cloudflare Workers, Edge Runtime, any JavaScript environment
243
+ - **Browsers**: All browsers (no polyfill required)
161
244
  - **TypeScript**: 5.0+ recommended
162
245
 
163
246
  ## Contributing
164
247
 
165
- MatchPattern follows the [WHATWG URLPattern specification](https://urlpattern.spec.whatwg.org/) while extending it for routing use cases.
248
+ MatchPattern follows the [WHATWG URLPattern specification](https://urlpattern.spec.whatwg.org/) while extending it for routing use cases.
166
249
 
167
250
  Report issues related to:
168
251
  - URLPattern compatibility problems
@@ -175,6 +258,6 @@ MIT - see LICENSE file for details.
175
258
 
176
259
  ## Acknowledgments
177
260
 
178
- - URLPattern specification by WHATWG
179
- - [urlpattern-polyfill](https://github.com/kenchris/urlpattern-polyfill) by Ken Christensen
180
- - Web Platform community
261
+ - [URLPattern specification](https://urlpattern.spec.whatwg.org/) by WHATWG
262
+ - Inspired by [path-to-regexp](https://github.com/pillarjs/path-to-regexp) and the URLPattern spec
263
+ - Web Platform community
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@b9g/match-pattern",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "devDependencies": {
5
- "@b9g/libuild": "^0.1.10"
5
+ "@b9g/libuild": "^0.1.11",
6
+ "bun-types": "latest"
6
7
  },
7
8
  "peerDependencies": {
8
9
  "urlpattern-polyfill": "^10.0.0"
@@ -13,21 +14,21 @@
13
14
  }
14
15
  },
15
16
  "type": "module",
16
- "types": "src/match-pattern.d.ts",
17
- "module": "src/match-pattern.js",
17
+ "types": "src/index.d.ts",
18
+ "module": "src/index.js",
18
19
  "exports": {
19
- "./match-pattern": {
20
- "types": "./src/match-pattern.d.ts",
21
- "import": "./src/match-pattern.js"
20
+ ".": {
21
+ "types": "./src/index.d.ts",
22
+ "import": "./src/index.js"
22
23
  },
23
- "./match-pattern.js": {
24
- "types": "./src/match-pattern.d.ts",
25
- "import": "./src/match-pattern.js"
24
+ "./index": {
25
+ "types": "./src/index.d.ts",
26
+ "import": "./src/index.js"
26
27
  },
27
- "./package.json": "./package.json",
28
- ".": {
29
- "types": "./src/match-pattern.d.ts",
30
- "import": "./src/match-pattern.js"
31
- }
28
+ "./index.js": {
29
+ "types": "./src/index.d.ts",
30
+ "import": "./src/index.js"
31
+ },
32
+ "./package.json": "./package.json"
32
33
  }
33
34
  }
package/src/index.d.ts ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Result of pattern matching
3
+ */
4
+ export interface MatchPatternResult {
5
+ params: Record<string, string>;
6
+ pathname: {
7
+ input: string;
8
+ groups: Record<string, string>;
9
+ };
10
+ search: {
11
+ input: string;
12
+ groups: Record<string, string>;
13
+ };
14
+ protocol: {
15
+ input: string;
16
+ groups: Record<string, string>;
17
+ };
18
+ hostname: {
19
+ input: string;
20
+ groups: Record<string, string>;
21
+ };
22
+ port: {
23
+ input: string;
24
+ groups: Record<string, string>;
25
+ };
26
+ username: {
27
+ input: string;
28
+ groups: Record<string, string>;
29
+ };
30
+ password: {
31
+ input: string;
32
+ groups: Record<string, string>;
33
+ };
34
+ hash: {
35
+ input: string;
36
+ groups: Record<string, string>;
37
+ };
38
+ inputs: (string | URLPatternInit)[];
39
+ }
40
+ /**
41
+ * Compiled pattern for fast matching
42
+ */
43
+ export interface CompiledPattern {
44
+ regex: RegExp;
45
+ paramNames: string[];
46
+ hasWildcard: boolean;
47
+ }
48
+ /**
49
+ * Segment types for parsed patterns
50
+ */
51
+ export type PatternSegment = {
52
+ type: "static";
53
+ value: string;
54
+ } | {
55
+ type: "param";
56
+ name: string;
57
+ } | {
58
+ type: "wildcard";
59
+ };
60
+ /**
61
+ * Result of parsing a simple pattern
62
+ */
63
+ export interface ParsedPattern {
64
+ segments: PatternSegment[];
65
+ paramNames: string[];
66
+ hasWildcard: boolean;
67
+ }
68
+ /**
69
+ * Check if a pathname pattern is "simple" (can be handled by radix tree)
70
+ * Simple patterns only have:
71
+ * - Static segments: /api/users
72
+ * - Basic named params: :id, :slug
73
+ * - Trailing wildcard: /*
74
+ *
75
+ * Complex patterns (return false) have:
76
+ * - Param constraints: :id(\d+)
77
+ * - Param modifiers: :path+, :path*, :id?
78
+ * - Regex groups: (\d+)
79
+ * - Optional groups: {/prefix}?
80
+ * - Escaped characters: \.
81
+ */
82
+ export declare function isSimplePattern(pathname: string): boolean;
83
+ /**
84
+ * Parse a simple pathname pattern into segments for radix tree insertion
85
+ * Returns null if the pattern is complex (use regex instead)
86
+ *
87
+ * Examples:
88
+ * - "/api/users" -> [{ type: "static", value: "/api/users" }]
89
+ * - "/users/:id" -> [{ type: "static", value: "/users/" }, { type: "param", name: "id" }]
90
+ * - "/files/*" -> [{ type: "static", value: "/files/" }, { type: "wildcard" }]
91
+ */
92
+ export declare function parseSimplePattern(pathname: string): ParsedPattern | null;
93
+ /**
94
+ * Compile pathname pattern to RegExp with full URLPattern syntax support
95
+ * @param pathname The pathname pattern to compile
96
+ * @param encodeChars Whether to percent-encode characters that aren't allowed in URL paths (default true)
97
+ * @param ignoreCase Whether to make the regex case-insensitive
98
+ */
99
+ export declare function compilePathname(pathname: string, encodeChars?: boolean, ignoreCase?: boolean): CompiledPattern;
100
+ /**
101
+ * Options for URLPattern/MatchPattern construction
102
+ */
103
+ export interface URLPatternOptions {
104
+ ignoreCase?: boolean;
105
+ }
106
+ /**
107
+ * Input type for URLPattern/MatchPattern
108
+ */
109
+ type URLPatternInit = {
110
+ protocol?: string;
111
+ hostname?: string;
112
+ port?: string;
113
+ pathname?: string;
114
+ search?: string;
115
+ hash?: string;
116
+ username?: string;
117
+ password?: string;
118
+ baseURL?: string;
119
+ };
120
+ /**
121
+ * URLPattern - Strict WPT-compliant URL pattern matching
122
+ *
123
+ * Differences from MatchPattern:
124
+ * - Throws for relative patterns without baseURL
125
+ * - Uses regex for search matching (order-dependent)
126
+ * - No & syntax support
127
+ */
128
+ export declare class URLPattern {
129
+ #private;
130
+ get pathname(): string;
131
+ get search(): string;
132
+ get protocol(): string;
133
+ get hostname(): string;
134
+ get port(): string;
135
+ get username(): string;
136
+ get password(): string;
137
+ get hash(): string;
138
+ constructor(input?: string | URLPatternInit, baseURLOrOptions?: string | URLPatternOptions, options?: URLPatternOptions);
139
+ test(input?: string | URL | URLPatternInit, baseURL?: string): boolean;
140
+ exec(input: string | URL | URLPatternInit, baseURL?: string): MatchPatternResult | null;
141
+ }
142
+ /**
143
+ * MatchPattern - URL pattern matching with conveniences for routing
144
+ *
145
+ * Features:
146
+ * - Relative paths without baseURL ("/users/:id" works)
147
+ * - & syntax for search params ("/api&format=json")
148
+ * - Order-independent search matching
149
+ */
150
+ export declare class MatchPattern {
151
+ #private;
152
+ get pathname(): string;
153
+ get search(): string | undefined;
154
+ get protocol(): string | undefined;
155
+ get hostname(): string | undefined;
156
+ get port(): string | undefined;
157
+ get username(): string | undefined;
158
+ get password(): string | undefined;
159
+ get hash(): string | undefined;
160
+ constructor(input?: string | URLPatternInit, baseURLOrOptions?: string | URLPatternOptions, options?: URLPatternOptions);
161
+ test(input?: string | URL | URLPatternInit, baseURL?: string): boolean;
162
+ exec(input: string | URL | URLPatternInit, baseURL?: string): MatchPatternResult | null;
163
+ }
164
+ export {};