@b9g/match-pattern 0.1.6 → 0.2.0-beta.0

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 ~40-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';
24
+
25
+ // URLPattern: 100% WPT-compliant, ~60x faster than polyfill/native
26
+ const strict = new URLPattern({ pathname: '/api/posts/:id' });
19
27
 
20
- // Create patterns with enhanced string syntax
28
+ // MatchPattern: Same performance + DX improvements
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
@@ -35,25 +66,36 @@ if (pattern.test(url)) {
35
66
  URLPattern requires exact parameter order. MatchPattern allows any order:
36
67
 
37
68
  ```javascript
38
- const pattern = new MatchPattern({ search: 'type=:type&sort=:sort' });
69
+ 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
- const pattern = new MatchPattern({ search: 'q=:query' });
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
@@ -66,18 +108,18 @@ const result = pattern.exec('/api/v1/posts/123?format=json&page=1');
66
108
 
67
109
  // URLPattern: result.pathname.groups + result.search.groups (separate)
68
110
  // MatchPattern: result.params (unified)
69
- console.log(result.params); // { version: 'v1', id: '123', format: 'json', page: '1' }
111
+ console.log(result.params); // {version: 'v1', id: '123', format: 'json', page: '1'}
70
112
  ```
71
113
 
72
114
  ### 4. Enhanced String Pattern Syntax
73
115
 
74
- MatchPattern supports convenient string patterns with `&` separator:
116
+ It's not possible to separate pathname from search with `?` because the syntax is used to indicate optionality, so MatchPattern supports convenient string patterns with `&` separator:
75
117
 
76
118
  ```javascript
77
- // Pathname only
119
+ // Pathname only (URLPattern throws a TypeError when passing a relative path without a baseURL)
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,9 +129,9 @@ 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
 
@@ -100,69 +142,155 @@ MatchPattern does not automatically normalize trailing slashes. Use explicit pat
100
142
  ```javascript
101
143
  // Exact matching
102
144
  const exactPattern = new MatchPattern('/api/posts/:id');
103
- exactPattern.test('/api/posts/123'); // true
104
- exactPattern.test('/api/posts/123/'); // false
145
+ exactPattern.test('/api/posts/123'); // true
146
+ exactPattern.test('/api/posts/123/'); // false
105
147
 
106
148
  // Optional trailing slash
107
149
  const flexiblePattern = new MatchPattern('/api/posts/:id{/}?');
108
- flexiblePattern.test('/api/posts/123'); // true
109
- flexiblePattern.test('/api/posts/123/'); // true
150
+ flexiblePattern.test('/api/posts/123'); // true
151
+ flexiblePattern.test('/api/posts/123/'); // true
110
152
  ```
111
153
 
112
- ## Known Limitations
154
+ ## Implementation Notes
155
+
156
+ ### Direct RegExp Compilation
113
157
 
114
- ### Cross-Implementation Differences
158
+ 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:
159
+ - **Performance**: ~40-60x faster than the URLPattern polyfill and native implementations
160
+ - **Consistency**: Same behavior across all JavaScript runtimes
161
+ - **Zero dependencies**: No polyfill required
162
+ - **Simplicity**: Direct pattern-to-RegExp compilation with minimal overhead
115
163
 
116
- The polyfill and native browser implementations can return different results for edge cases.
164
+ ### URLPattern Spec Compliance
117
165
 
118
- **Issue:** [kenchris/urlpattern-polyfill#129](https://github.com/kenchris/urlpattern-polyfill/issues/129)
166
+ The `URLPattern` class passes 100% of the Web Platform Tests (755 tests). It implements the full URLPattern specification:
119
167
 
120
- **Impact:** Results may vary between Node.js, browsers, and other runtimes.
168
+ - Named parameters: `:id`, `:id(\d+)`
169
+ - Optional parameters: `:id?`
170
+ - Repeat modifiers: `:path+`, `:path*`
171
+ - Wildcards: `*`
172
+ - Regex groups: `(\d+)`
173
+ - Explicit delimiters: `{/old}?`
174
+ - Escaped characters: `\.`
175
+ - Protocol, hostname, port, pathname, search, and hash matching
176
+ - baseURL parameter for relative pattern resolution
177
+ - ignoreCase option
121
178
 
122
- **Testing:** Use Playwright for cross-browser validation in production applications.
179
+ `MatchPattern` intentionally deviates from strict spec compliance in two areas to provide better routing ergonomics:
180
+ - Allows relative patterns without baseURL (convenience for routing)
181
+ - Order-independent search parameter matching
182
+
183
+ ## Exports
184
+
185
+ ### Classes
186
+
187
+ - `URLPattern` - 100% WPT-compliant URLPattern implementation
188
+ - `MatchPattern` - URLPattern with routing enhancements (order-independent search params, unified params)
189
+
190
+ ### Types
123
191
 
124
- ### TypeScript Compatibility
192
+ - `MatchPatternResult` - Result type for MatchPattern.exec()
193
+ - `URLPatternOptions` - Options for URLPattern constructor (ignoreCase, etc.)
194
+ - `ParsedPattern` - Parsed pattern structure
195
+ - `PatternSegment` - Individual segment of a parsed pattern
196
+ - `CompiledPattern` - Internal compiled pattern representation
125
197
 
126
- Node.js and polyfill URLPattern types have slight differences in their TypeScript definitions.
198
+ ### Utility Functions
127
199
 
128
- **Issue:** [kenchris/urlpattern-polyfill#135](https://github.com/kenchris/urlpattern-polyfill/issues/135)
200
+ Advanced functions for pattern inspection and compilation for optimized routers
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
+ class URLPattern {
212
+ constructor(input?: string | URLPatternInit, baseURL?: string, options?: URLPatternOptions)
213
+
214
+ test(input: string | URL | URLPatternInit, baseURL?: string): boolean
215
+ exec(input: string | URL | URLPatternInit, baseURL?: string): URLPatternResult | null
216
+
217
+ readonly protocol: string
218
+ readonly username: string
219
+ readonly password: string
220
+ readonly hostname: string
221
+ readonly port: string
222
+ readonly pathname: string
223
+ readonly search: string
224
+ readonly hash: string
225
+ }
226
+ ```
227
+
228
+ ### MatchPattern
229
+
230
+ ```typescript
231
+ class MatchPattern {
232
+ constructor(input: string | URLPatternInit, baseURL?: string)
233
+
234
+ test(input: string | URL): boolean
235
+ exec(input: string | URL): MatchPatternResult | null
236
+ }
138
237
  ```
139
238
 
140
- ### Methods
239
+ ### Utility Functions
240
+
241
+ Advanced functions for pattern inspection and compilation (used by router optimizations):
141
242
 
142
243
  ```typescript
143
- // Enhanced methods with unified params
144
- test(input: string | URL): boolean
145
- exec(input: string | URL): MatchPatternResult | null
244
+ // Check if a pathname pattern contains only literal segments and named parameters
245
+ function isSimplePattern(pathname: string): boolean
246
+
247
+ // Parse a simple pattern into its component segments
248
+ function parseSimplePattern(pathname: string): ParsedPattern | null
249
+
250
+ // Compile a pathname pattern to optimized RegExp
251
+ function compilePathname(
252
+ pathname: string,
253
+ options?: { ignoreCase?: boolean }
254
+ ): CompiledPattern
146
255
  ```
147
256
 
148
257
  ### Types
149
258
 
150
259
  ```typescript
151
260
  interface MatchPatternResult extends URLPatternResult {
152
- params: Record<string, string>; // Unified parameters from all sources
261
+ params: Record<string, string>; // Unified parameters from pathname and search
262
+ }
263
+
264
+ interface URLPatternOptions {
265
+ ignoreCase?: boolean; // Case-insensitive matching
266
+ }
267
+
268
+ interface ParsedPattern {
269
+ segments: PatternSegment[];
270
+ paramNames: string[];
271
+ }
272
+
273
+ type PatternSegment =
274
+ | { type: 'literal'; value: string }
275
+ | { type: 'param'; name: string; pattern?: string }
276
+ | { type: 'wildcard' }
277
+ | { type: 'group'; segments: PatternSegment[] }
278
+
279
+ interface CompiledPattern {
280
+ regexp: RegExp;
281
+ paramNames: string[];
153
282
  }
154
283
  ```
155
284
 
156
285
  ## Compatibility
157
286
 
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
287
+ - **Runtimes**: Node, Deno, Bun, Cloudflare Workers, Edge Runtime, any JavaScript environment
288
+ - **Browsers**: All browsers (no polyfill required)
161
289
  - **TypeScript**: 5.0+ recommended
162
290
 
163
291
  ## Contributing
164
292
 
165
- MatchPattern follows the [WHATWG URLPattern specification](https://urlpattern.spec.whatwg.org/) while extending it for routing use cases.
293
+ MatchPattern follows the [WHATWG URLPattern specification](https://urlpattern.spec.whatwg.org/) while extending it for routing use cases.
166
294
 
167
295
  Report issues related to:
168
296
  - URLPattern compatibility problems
@@ -171,10 +299,4 @@ Report issues related to:
171
299
 
172
300
  ## License
173
301
 
174
- MIT - see LICENSE file for details.
175
-
176
- ## Acknowledgments
177
-
178
- - URLPattern specification by WHATWG
179
- - [urlpattern-polyfill](https://github.com/kenchris/urlpattern-polyfill) by Ken Christensen
180
- - Web Platform community
302
+ MIT
package/package.json CHANGED
@@ -1,34 +1,25 @@
1
1
  {
2
2
  "name": "@b9g/match-pattern",
3
- "version": "0.1.6",
3
+ "version": "0.2.0-beta.0",
4
4
  "devDependencies": {
5
- "@b9g/libuild": "^0.1.11",
6
- "bun-types": "latest"
7
- },
8
- "peerDependencies": {
9
- "urlpattern-polyfill": "^10.0.0"
10
- },
11
- "peerDependenciesMeta": {
12
- "urlpattern-polyfill": {
13
- "optional": true
14
- }
5
+ "@b9g/libuild": "^0.1.18"
15
6
  },
16
7
  "type": "module",
17
- "types": "src/match-pattern.d.ts",
18
- "module": "src/match-pattern.js",
8
+ "types": "src/index.d.ts",
9
+ "module": "src/index.js",
19
10
  "exports": {
20
- "./match-pattern": {
21
- "types": "./src/match-pattern.d.ts",
22
- "import": "./src/match-pattern.js"
11
+ ".": {
12
+ "types": "./src/index.d.ts",
13
+ "import": "./src/index.js"
23
14
  },
24
- "./match-pattern.js": {
25
- "types": "./src/match-pattern.d.ts",
26
- "import": "./src/match-pattern.js"
15
+ "./index": {
16
+ "types": "./src/index.d.ts",
17
+ "import": "./src/index.js"
27
18
  },
28
- "./package.json": "./package.json",
29
- ".": {
30
- "types": "./src/match-pattern.d.ts",
31
- "import": "./src/match-pattern.js"
32
- }
19
+ "./index.js": {
20
+ "types": "./src/index.d.ts",
21
+ "import": "./src/index.js"
22
+ },
23
+ "./package.json": "./package.json"
33
24
  }
34
25
  }
package/src/index.d.ts ADDED
@@ -0,0 +1,168 @@
1
+ /**
2
+ * High-performance URLPattern-compatible implementation for web routing with
3
+ * enhanced search parameter handling.
4
+ */
5
+ /**
6
+ * Result of pattern matching
7
+ */
8
+ export interface MatchPatternResult {
9
+ params: Record<string, string>;
10
+ pathname: {
11
+ input: string;
12
+ groups: Record<string, string>;
13
+ };
14
+ search: {
15
+ input: string;
16
+ groups: Record<string, string>;
17
+ };
18
+ protocol: {
19
+ input: string;
20
+ groups: Record<string, string>;
21
+ };
22
+ hostname: {
23
+ input: string;
24
+ groups: Record<string, string>;
25
+ };
26
+ port: {
27
+ input: string;
28
+ groups: Record<string, string>;
29
+ };
30
+ username: {
31
+ input: string;
32
+ groups: Record<string, string>;
33
+ };
34
+ password: {
35
+ input: string;
36
+ groups: Record<string, string>;
37
+ };
38
+ hash: {
39
+ input: string;
40
+ groups: Record<string, string>;
41
+ };
42
+ inputs: (string | URLPatternInit)[];
43
+ }
44
+ /**
45
+ * Compiled pattern for fast matching
46
+ */
47
+ export interface CompiledPattern {
48
+ regex: RegExp;
49
+ paramNames: string[];
50
+ hasWildcard: boolean;
51
+ }
52
+ /**
53
+ * Segment types for parsed patterns
54
+ */
55
+ export type PatternSegment = {
56
+ type: "static";
57
+ value: string;
58
+ } | {
59
+ type: "param";
60
+ name: string;
61
+ } | {
62
+ type: "wildcard";
63
+ };
64
+ /**
65
+ * Result of parsing a simple pattern
66
+ */
67
+ export interface ParsedPattern {
68
+ segments: PatternSegment[];
69
+ paramNames: string[];
70
+ hasWildcard: boolean;
71
+ }
72
+ /**
73
+ * Check if a pathname pattern is "simple" (can be handled by radix tree)
74
+ * Simple patterns only have:
75
+ * - Static segments: /api/users
76
+ * - Basic named params: :id, :slug
77
+ * - Trailing wildcard: /*
78
+ *
79
+ * Complex patterns (return false) have:
80
+ * - Param constraints: :id(\d+)
81
+ * - Param modifiers: :path+, :path*, :id?
82
+ * - Regex groups: (\d+)
83
+ * - Optional groups: {/prefix}?
84
+ * - Escaped characters: \.
85
+ */
86
+ export declare function isSimplePattern(pathname: string): boolean;
87
+ /**
88
+ * Parse a simple pathname pattern into segments for radix tree insertion
89
+ * Returns null if the pattern is complex (use regex instead)
90
+ *
91
+ * Examples:
92
+ * - "/api/users" -> [{ type: "static", value: "/api/users" }]
93
+ * - "/users/:id" -> [{ type: "static", value: "/users/" }, { type: "param", name: "id" }]
94
+ * - "/files/*" -> [{ type: "static", value: "/files/" }, { type: "wildcard" }]
95
+ */
96
+ export declare function parseSimplePattern(pathname: string): ParsedPattern | null;
97
+ /**
98
+ * Compile pathname pattern to RegExp with full URLPattern syntax support
99
+ * @param pathname The pathname pattern to compile
100
+ * @param encodeChars Whether to percent-encode characters that aren't allowed in URL paths (default true)
101
+ * @param ignoreCase Whether to make the regex case-insensitive
102
+ */
103
+ export declare function compilePathname(pathname: string, encodeChars?: boolean, ignoreCase?: boolean): CompiledPattern;
104
+ /**
105
+ * Options for URLPattern/MatchPattern construction
106
+ */
107
+ export interface URLPatternOptions {
108
+ ignoreCase?: boolean;
109
+ }
110
+ /**
111
+ * Input type for URLPattern/MatchPattern
112
+ */
113
+ type URLPatternInit = {
114
+ protocol?: string;
115
+ hostname?: string;
116
+ port?: string;
117
+ pathname?: string;
118
+ search?: string;
119
+ hash?: string;
120
+ username?: string;
121
+ password?: string;
122
+ baseURL?: string;
123
+ };
124
+ /**
125
+ * URLPattern - Strict WPT-compliant URL pattern matching
126
+ *
127
+ * Differences from MatchPattern:
128
+ * - Throws for relative patterns without baseURL
129
+ * - Uses regex for search matching (order-dependent)
130
+ * - No & syntax support
131
+ */
132
+ export declare class URLPattern {
133
+ #private;
134
+ get pathname(): string;
135
+ get search(): string;
136
+ get protocol(): string;
137
+ get hostname(): string;
138
+ get port(): string;
139
+ get username(): string;
140
+ get password(): string;
141
+ get hash(): string;
142
+ constructor(input?: string | URLPatternInit, baseURLOrOptions?: string | URLPatternOptions, options?: URLPatternOptions);
143
+ test(input?: string | URL | URLPatternInit, baseURL?: string): boolean;
144
+ exec(input: string | URL | URLPatternInit, baseURL?: string): MatchPatternResult | null;
145
+ }
146
+ /**
147
+ * MatchPattern - URL pattern matching with conveniences for routing
148
+ *
149
+ * Features:
150
+ * - Relative paths without baseURL ("/users/:id" works)
151
+ * - & syntax for search params ("/api&format=json")
152
+ * - Order-independent search matching
153
+ */
154
+ export declare class MatchPattern {
155
+ #private;
156
+ get pathname(): string;
157
+ get search(): string | undefined;
158
+ get protocol(): string | undefined;
159
+ get hostname(): string | undefined;
160
+ get port(): string | undefined;
161
+ get username(): string | undefined;
162
+ get password(): string | undefined;
163
+ get hash(): string | undefined;
164
+ constructor(input?: string | URLPatternInit, baseURLOrOptions?: string | URLPatternOptions, options?: URLPatternOptions);
165
+ test(input?: string | URL | URLPatternInit, baseURL?: string): boolean;
166
+ exec(input: string | URL | URLPatternInit, baseURL?: string): MatchPatternResult | null;
167
+ }
168
+ export {};