@b9g/match-pattern 0.1.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 ADDED
@@ -0,0 +1,180 @@
1
+ # MatchPattern
2
+
3
+ Extended URLPattern for better routing with enhanced search parameter handling.
4
+
5
+ ## Overview
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.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @b9g/match-pattern
13
+ ```
14
+
15
+ ## Basic Usage
16
+
17
+ ```javascript
18
+ import { MatchPattern } from '@b9g/match-pattern';
19
+
20
+ // Create patterns with enhanced string syntax
21
+ const pattern = new MatchPattern('/api/posts/:id&format=:format');
22
+ const url = new URL('http://example.com/api/posts/123?format=json&page=1');
23
+
24
+ if (pattern.test(url)) {
25
+ const result = pattern.exec(url);
26
+ console.log(result.params);
27
+ // { id: '123', format: 'json', page: '1' }
28
+ }
29
+ ```
30
+
31
+ ## Key Differences from URLPattern
32
+
33
+ ### 1. Order-Independent Search Parameters
34
+
35
+ URLPattern requires exact parameter order. MatchPattern allows any order:
36
+
37
+ ```javascript
38
+ const pattern = new MatchPattern({ search: 'type=:type&sort=:sort' });
39
+
40
+ // URLPattern: Only first URL matches
41
+ // MatchPattern: Both URLs match
42
+ pattern.test('/?type=blog&sort=date'); // ✅ Both: true
43
+ pattern.test('/?sort=date&type=blog'); // ✅ MatchPattern: true, URLPattern: false
44
+ ```
45
+
46
+ ### 2. Non-Exhaustive Search Matching
47
+
48
+ URLPattern rejects extra parameters. MatchPattern captures them:
49
+
50
+ ```javascript
51
+ const pattern = new MatchPattern({ search: 'q=:query' });
52
+ const result = pattern.exec('/?q=hello&page=1&limit=10');
53
+
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);
57
+ ```
58
+
59
+ ### 3. Unified Parameter Object
60
+
61
+ URLPattern separates pathname and search groups. MatchPattern merges everything:
62
+
63
+ ```javascript
64
+ const pattern = new MatchPattern('/api/:version/posts/:id&format=:format');
65
+ const result = pattern.exec('/api/v1/posts/123?format=json&page=1');
66
+
67
+ // URLPattern: result.pathname.groups + result.search.groups (separate)
68
+ // MatchPattern: result.params (unified)
69
+ console.log(result.params); // { version: 'v1', id: '123', format: 'json', page: '1' }
70
+ ```
71
+
72
+ ### 4. Enhanced String Pattern Syntax
73
+
74
+ MatchPattern supports convenient string patterns with `&` separator:
75
+
76
+ ```javascript
77
+ // Pathname only
78
+ new MatchPattern('/api/posts/:id')
79
+
80
+ // Pathname with search parameters
81
+ new MatchPattern('/api/posts/:id&format=:format&page=:page')
82
+
83
+ // Search parameters only
84
+ new MatchPattern('&q=:query&sort=:sort')
85
+
86
+ // Full URL patterns
87
+ new MatchPattern('https://api.example.com/v1/posts/:id&format=:format')
88
+
89
+ // Object syntax (same as URLPattern, enhanced behavior)
90
+ new MatchPattern({
91
+ pathname: '/api/posts/:id',
92
+ search: 'format=:format'
93
+ })
94
+ ```
95
+
96
+ ## Trailing Slash Normalization
97
+
98
+ URLPattern has inconsistent behavior with trailing slashes that can cause unexpected matches.
99
+
100
+ **Issue:** [kenchris/urlpattern-polyfill#131](https://github.com/kenchris/urlpattern-polyfill/issues/131)
101
+
102
+ **Solution:** ✅ Automatic trailing slash normalization implemented
103
+
104
+ ```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)
110
+ ```
111
+
112
+ ## Known Limitations
113
+
114
+ ### Cross-Implementation Differences
115
+
116
+ The polyfill and native browser implementations can return different results for edge cases.
117
+
118
+ **Issue:** [kenchris/urlpattern-polyfill#129](https://github.com/kenchris/urlpattern-polyfill/issues/129)
119
+
120
+ **Impact:** Results may vary between Node.js, browsers, and other runtimes.
121
+
122
+ **Testing:** Use Playwright for cross-browser validation in production applications.
123
+
124
+ ### TypeScript Compatibility
125
+
126
+ Node.js and polyfill URLPattern types have slight differences in their TypeScript definitions.
127
+
128
+ **Issue:** [kenchris/urlpattern-polyfill#135](https://github.com/kenchris/urlpattern-polyfill/issues/135)
129
+
130
+ **Solution:** MatchPattern uses canonical WHATWG specification types internally.
131
+
132
+ ## API Reference
133
+
134
+ ### Constructor
135
+
136
+ ```typescript
137
+ new MatchPattern(input: string | URLPatternInit, baseURL?: string)
138
+ ```
139
+
140
+ ### Methods
141
+
142
+ ```typescript
143
+ // Enhanced methods with unified params
144
+ test(input: string | URL): boolean
145
+ exec(input: string | URL): MatchPatternResult | null
146
+ ```
147
+
148
+ ### Types
149
+
150
+ ```typescript
151
+ interface MatchPatternResult extends URLPatternResult {
152
+ params: Record<string, string>; // Unified parameters from all sources
153
+ }
154
+ ```
155
+
156
+ ## Compatibility
157
+
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
161
+ - **TypeScript**: 5.0+ recommended
162
+
163
+ ## Contributing
164
+
165
+ MatchPattern follows the [WHATWG URLPattern specification](https://urlpattern.spec.whatwg.org/) while extending it for routing use cases.
166
+
167
+ Report issues related to:
168
+ - URLPattern compatibility problems
169
+ - Performance issues with complex patterns
170
+ - Cross-runtime behavior differences
171
+
172
+ ## License
173
+
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
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@b9g/match-pattern",
3
+ "version": "0.1.0",
4
+ "devDependencies": {
5
+ "@b9g/libuild": "^0.1.10"
6
+ },
7
+ "type": "module",
8
+ "types": "src/index.d.ts",
9
+ "module": "src/index.js",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./src/index.d.ts",
13
+ "import": "./src/index.js"
14
+ },
15
+ "./index": {
16
+ "types": "./src/index.d.ts",
17
+ "import": "./src/index.js"
18
+ },
19
+ "./index.js": {
20
+ "types": "./src/index.d.ts",
21
+ "import": "./src/index.js"
22
+ },
23
+ "./match-pattern": {
24
+ "types": "./src/match-pattern.d.ts",
25
+ "import": "./src/match-pattern.js"
26
+ },
27
+ "./match-pattern.js": {
28
+ "types": "./src/match-pattern.d.ts",
29
+ "import": "./src/match-pattern.js"
30
+ },
31
+ "./package.json": "./package.json"
32
+ }
33
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { MatchPattern, type MatchPatternResult } from "./match-pattern.ts";
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ /// <reference types="./index.d.ts" />
2
+ // src/index.ts
3
+ import { MatchPattern } from "./match-pattern.js";
4
+ export {
5
+ MatchPattern
6
+ };
@@ -0,0 +1,43 @@
1
+ declare let URLPattern: any;
2
+ type URLPatternInput = URLPatternInit | string;
3
+ interface URLPatternComponentResult {
4
+ input: string;
5
+ groups: Record<string, string | undefined>;
6
+ }
7
+ interface URLPatternResult {
8
+ inputs: URLPatternInput[];
9
+ protocol: URLPatternComponentResult;
10
+ username: URLPatternComponentResult;
11
+ password: URLPatternComponentResult;
12
+ hostname: URLPatternComponentResult;
13
+ port: URLPatternComponentResult;
14
+ pathname: URLPatternComponentResult;
15
+ search: URLPatternComponentResult;
16
+ hash: URLPatternComponentResult;
17
+ }
18
+ /**
19
+ * Enhanced URLPattern result that includes unified params
20
+ */
21
+ export interface MatchPatternResult extends URLPatternResult {
22
+ params: Record<string, string>;
23
+ }
24
+ /**
25
+ * MatchPattern extends URLPattern with enhanced routing capabilities:
26
+ * - Order-independent search parameter matching
27
+ * - Non-exhaustive search matching (extra params allowed)
28
+ * - Unified parameter extraction (pathname + search + extras)
29
+ * - Enhanced string pattern parsing with & syntax
30
+ */
31
+ export declare class MatchPattern extends URLPattern {
32
+ private _originalInput;
33
+ constructor(input: string | URLPatternInit, baseURL?: string);
34
+ /**
35
+ * Enhanced exec that returns unified params object with trailing slash normalization
36
+ */
37
+ exec(input: string | URL): MatchPatternResult | null;
38
+ /**
39
+ * Enhanced test with order-independent search parameter matching and trailing slash normalization
40
+ */
41
+ test(input: string | URL): boolean;
42
+ }
43
+ export {};
@@ -0,0 +1,214 @@
1
+ /// <reference types="./match-pattern.d.ts" />
2
+ // src/match-pattern.ts
3
+ var URLPattern = globalThis.URLPattern;
4
+ if (!URLPattern) {
5
+ await import("urlpattern-polyfill");
6
+ URLPattern = globalThis.URLPattern;
7
+ }
8
+ var MatchPattern = class extends URLPattern {
9
+ _originalInput;
10
+ constructor(input, baseURL) {
11
+ let processedInput = input;
12
+ if (typeof input === "string") {
13
+ processedInput = parseStringPattern(input);
14
+ }
15
+ const normalizedInput = normalizePatternTrailingSlash(processedInput);
16
+ if (baseURL !== void 0) {
17
+ super(normalizedInput, baseURL);
18
+ } else {
19
+ super(normalizedInput);
20
+ }
21
+ this._originalInput = normalizedInput;
22
+ }
23
+ /**
24
+ * Enhanced exec that returns unified params object with trailing slash normalization
25
+ */
26
+ exec(input) {
27
+ if (!this.test(input)) {
28
+ return null;
29
+ }
30
+ const url = typeof input === "string" ? new URL(input) : input;
31
+ const normalizedUrl = normalizeTrailingSlash(url);
32
+ const result = super.exec(normalizedUrl);
33
+ if (result) {
34
+ const enhancedResult = {
35
+ ...result,
36
+ params: extractUnifiedParams(result, input)
37
+ // Use original input for search params
38
+ };
39
+ return enhancedResult;
40
+ }
41
+ return buildCustomResult(this, input);
42
+ }
43
+ /**
44
+ * Enhanced test with order-independent search parameter matching and trailing slash normalization
45
+ */
46
+ test(input) {
47
+ const url = typeof input === "string" ? new URL(input) : input;
48
+ const normalizedUrl = normalizeTrailingSlash(url);
49
+ if (!this.search || this.search === "*") {
50
+ return super.test(normalizedUrl);
51
+ }
52
+ const pathPatternInit = typeof this._originalInput === "string" ? { pathname: this._originalInput } : { ...this._originalInput, search: void 0 };
53
+ const normalizedPattern = normalizePatternTrailingSlash(pathPatternInit);
54
+ const pathPattern = new URLPattern(normalizedPattern);
55
+ if (!pathPattern.test(normalizedUrl)) {
56
+ return false;
57
+ }
58
+ return testSearchParameters(this.search, url.searchParams);
59
+ }
60
+ };
61
+ function parseStringPattern(pattern) {
62
+ if (pattern.includes("://")) {
63
+ const ampIndex2 = pattern.indexOf("&");
64
+ if (ampIndex2 === -1) {
65
+ return pattern;
66
+ }
67
+ const urlPart = pattern.slice(0, ampIndex2);
68
+ const search2 = pattern.slice(ampIndex2 + 1);
69
+ try {
70
+ const url = new URL(urlPart.replace(/:(\w+)/g, "placeholder"));
71
+ return {
72
+ protocol: urlPart.split("://")[0],
73
+ hostname: url.hostname,
74
+ pathname: url.pathname.replace(/placeholder/g, ":$1"),
75
+ // Restore params
76
+ search: search2
77
+ };
78
+ } catch {
79
+ return pattern;
80
+ }
81
+ }
82
+ const ampIndex = pattern.indexOf("&");
83
+ if (ampIndex === -1) {
84
+ return { pathname: pattern };
85
+ }
86
+ if (ampIndex === 0) {
87
+ return { search: pattern.slice(1) };
88
+ }
89
+ const pathname = pattern.slice(0, ampIndex);
90
+ const search = pattern.slice(ampIndex + 1);
91
+ return { pathname, search };
92
+ }
93
+ function extractUnifiedParams(result, url) {
94
+ const params = {};
95
+ if (result.pathname?.groups) {
96
+ for (const [key, value] of Object.entries(result.pathname.groups)) {
97
+ if (value !== void 0) {
98
+ params[key] = value;
99
+ }
100
+ }
101
+ }
102
+ if (result.search?.groups) {
103
+ const actualUrl = typeof url === "string" ? new URL(url) : url;
104
+ const searchParams = actualUrl.searchParams;
105
+ for (const [key, value] of searchParams) {
106
+ params[key] = value;
107
+ }
108
+ } else if (typeof url !== "string") {
109
+ const actualUrl = url instanceof URL ? url : new URL(url);
110
+ for (const [key, value] of actualUrl.searchParams) {
111
+ params[key] = value;
112
+ }
113
+ }
114
+ return params;
115
+ }
116
+ function normalizeTrailingSlash(url) {
117
+ const normalized = new URL(url.href);
118
+ if (normalized.pathname === "/") {
119
+ return normalized;
120
+ }
121
+ if (normalized.pathname.endsWith("/")) {
122
+ normalized.pathname = normalized.pathname.slice(0, -1);
123
+ }
124
+ return normalized;
125
+ }
126
+ function normalizePatternTrailingSlash(patternInit) {
127
+ if (typeof patternInit === "string") {
128
+ if (patternInit === "/" || patternInit === "") {
129
+ return patternInit;
130
+ }
131
+ return patternInit.endsWith("/") ? patternInit.slice(0, -1) : patternInit;
132
+ }
133
+ const normalized = { ...patternInit };
134
+ if (normalized.pathname && normalized.pathname !== "/") {
135
+ if (normalized.pathname.endsWith("/")) {
136
+ normalized.pathname = normalized.pathname.slice(0, -1);
137
+ }
138
+ }
139
+ return normalized;
140
+ }
141
+ function buildCustomResult(pattern, input) {
142
+ const url = typeof input === "string" ? new URL(input) : input;
143
+ const result = {
144
+ inputs: [input],
145
+ pathname: { input: url.pathname, groups: {} },
146
+ search: { input: url.search, groups: {} },
147
+ hash: { input: url.hash, groups: {} },
148
+ protocol: { input: url.protocol, groups: {} },
149
+ hostname: { input: url.hostname, groups: {} },
150
+ port: { input: url.port, groups: {} },
151
+ username: { input: url.username, groups: {} },
152
+ password: { input: url.password, groups: {} },
153
+ params: {}
154
+ };
155
+ if (pattern.pathname && pattern.pathname !== "*") {
156
+ const pathPattern = new URLPattern({ pathname: pattern.pathname });
157
+ const pathResult = pathPattern.exec(url);
158
+ if (pathResult?.pathname?.groups) {
159
+ result.pathname.groups = pathResult.pathname.groups;
160
+ }
161
+ }
162
+ if (pattern.search && pattern.search !== "*") {
163
+ const searchParams = parseSearchPattern(pattern.search);
164
+ const actualParams = url.searchParams;
165
+ for (const [key, paramDef] of searchParams) {
166
+ if (actualParams.has(key)) {
167
+ if (paramDef.type === "named" && paramDef.name) {
168
+ result.search.groups[paramDef.name] = actualParams.get(key);
169
+ }
170
+ }
171
+ }
172
+ }
173
+ result.params = extractUnifiedParams(result, input);
174
+ return result;
175
+ }
176
+ function testSearchParameters(searchPattern, actualParams) {
177
+ const patternParams = parseSearchPattern(searchPattern);
178
+ for (const [key, paramPattern] of patternParams) {
179
+ if (!actualParams.has(key)) {
180
+ return false;
181
+ }
182
+ if (paramPattern.type === "literal") {
183
+ if (actualParams.get(key) !== paramPattern.value) {
184
+ return false;
185
+ }
186
+ }
187
+ }
188
+ return true;
189
+ }
190
+ function parseSearchPattern(pattern) {
191
+ const params = /* @__PURE__ */ new Map();
192
+ const parts = pattern.split("&");
193
+ for (const part of parts) {
194
+ const [key, value] = part.split("=");
195
+ if (!key || !value)
196
+ continue;
197
+ if (value.startsWith(":")) {
198
+ const isOptional = value.endsWith("?");
199
+ params.set(key, {
200
+ type: "named",
201
+ name: value.slice(1, isOptional ? -1 : void 0),
202
+ optional: isOptional
203
+ });
204
+ } else if (value === "*") {
205
+ params.set(key, { type: "wildcard" });
206
+ } else {
207
+ params.set(key, { type: "literal", value });
208
+ }
209
+ }
210
+ return params;
211
+ }
212
+ export {
213
+ MatchPattern
214
+ };