{"_id":"@bengillies/url-generator","name":"@bengillies/url-generator","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@bengillies/url-generator","version":"0.1.0","description":"Lightweight URL/path building library based on URLPattern","type":"module","license":"BSD-3-Clause","homepage":"https://bengillies.github.io/url-generator/","repository":{"type":"git","url":"git+https://github.com/bengillies/url-generator.git"},"bugs":{"url":"https://github.com/bengillies/url-generator/issues"},"main":"./dist/url-generator.cjs","module":"./dist/url-generator.js","types":"./dist/types/index.d.ts","exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/url-generator.js","require":"./dist/url-generator.cjs"}},"publishConfig":{"access":"public"},"engines":{"node":">=24.2.0"},"scripts":{"start":"vite --config vite.demo.config.ts","build:demo":"vite build --config vite.demo.config.ts","dist":"vite build","release":"node scripts/publish-npm.mjs","lint":"eslint . --ext .ts --max-warnings 0","lint:fix":"npm run lint -- --fix","test":"vitest run","test:browser":"vitest run --project browser","test:node":"vitest run --project node","test:watch":"vitest","test:types":"tsc --noEmit -p tsconfig.vitest.json","transform:testdata":"node tests/fixtures/transform-testdata.ts"},"keywords":["routes","matcher","typescript","browser"],"author":{"name":"Ben Gillies"},"devDependencies":{"@eslint/js":"^10.0.1","@testing-library/dom":"^10.4.1","@testing-library/jest-dom":"^6.9.1","@types/debug":"^4.1.12","@types/node":"^25.5.0","@typescript-eslint/eslint-plugin":"^8.57.0","@typescript-eslint/parser":"^8.57.0","@vitest/browser":"^4.0.7","@vitest/browser-playwright":"^4.0.7","@vitest/coverage-v8":"^4.0.7","eslint":"^10.0.3","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.5","globals":"^17.4.0","json5":"^2.2.3","playwright":"^1.58.2","prettier":"^3.8.1","typescript":"^5.9.3","vite":"^8.0.0","vite-plugin-dts":"^4.5.4","vitest":"^4.0.7"},"gitHead":"3b4f688428504d663b0548ab22f3043e65e38a80","_id":"@bengillies/url-generator@0.1.0","_nodeVersion":"24.14.0","_npmVersion":"11.9.0","dist":{"integrity":"sha512-sf2ckAxiX4X5gsn67o8jTXKNWhOxRekk7EaCTiqbWjJxVliUgYh6TwjQK3AfD32cD35buUmH9RECZCmW7Km2nA==","shasum":"65b614a865a1c643f6f193edee55b0cbee9a9227","tarball":"https://registry.npmjs.org/@bengillies/url-generator/-/url-generator-0.1.0.tgz","fileCount":9,"unpackedSize":80516,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICqXSlu2xW/gmvluIyBmVx5Ef9TzcQQxoewCO+g5gspFAiEAxH+DegRUVjopl82IRN4qk3VW8FlnPldtbHCKsiF7bz8="}]},"_npmUser":{"name":"bengillies","email":"ben@bengillies.net"},"directories":{},"maintainers":[{"name":"bengillies","email":"ben@bengillies.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/url-generator_0.1.0_1774103177350_0.1305679878335131"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-21T14:26:17.246Z","0.1.0":"2026-03-21T14:26:17.537Z","modified":"2026-03-21T14:26:17.755Z"},"maintainers":[{"name":"bengillies","email":"ben@bengillies.net"}],"description":"Lightweight URL/path building library based on URLPattern","homepage":"https://bengillies.github.io/url-generator/","keywords":["routes","matcher","typescript","browser"],"repository":{"type":"git","url":"git+https://github.com/bengillies/url-generator.git"},"author":{"name":"Ben Gillies"},"bugs":{"url":"https://github.com/bengillies/url-generator/issues"},"license":"BSD-3-Clause","readme":"# url-generator\n\nA minimal, URLPattern-first URL generator. It intentionally does the least possible beyond URLPattern itself so patterns can work in both directions: matching and generation. In other words, this is a small “reverse URLPattern” helper.\n\n## Requirements\n\n- Runtime support for `URLPattern` and `URL`.\n- Node: `>= 24.2.0`.\n- Works in modern browsers and Node.\n\n## Install\n\n```sh\nnpm install @bengillies/url-generator\n```\n\n## Quick start\n\n```ts\nimport { generate } from '@bengillies/url-generator';\n\nconst decode = (match) => {\n  if (!match) throw new Error('No match');\n\n  for (const param in match) {\n      for (const key in match[param].groups) {\n          const value = match[param].groups[key];\n\n          match[param].groups[key] = param === 'search' ?\n            decodeURIComponent(value.replace(/\\+/g, '%20')) :\n            decodeURIComponent(value);\n      }\n  }\n\n  return match;\n};\n\nconst pattern = new URLPattern(\n  'https://example.com/users/:id\\\\?tag=:tag#section-:section',\n);\nconst input = 'https://example.com/users/alice?tag=urls+are+cool#section-1';\n\nconst url = generate(pattern, decode(pattern.exec(input)));\n\nconsole.log(url.href);\n// https://example.com/users/alice?tag=urls+are+cool#section-1\n```\n\n## API\n\n### `generate(pattern, params) => URL`\n\nBuilds a URL from a `URLPattern` and a parameter map. Returns a `URL` instance.\n\n```ts\nimport { generate, type Params } from '@bengillies/url-generator';\n\nconst pattern = new URLPattern({ pathname: '/posts/:slug' });\nconst params: Params = {\n  protocol: { groups: { 0: 'https' } },\n  hostname: { groups: { 0: 'example.com' } },\n  pathname: { groups: { slug: 'hello-world' } },\n};\n\nconst url = generate(pattern, params);\n// https://example.com/posts/hello-world\n```\n\n### Params shape\n\n`Params` is a partial record keyed by URLPattern components. Each component can supply:\n\n- `groups`: parameter values by name or position.\n- `stringify` (optional): for non-string values.\n- `disableEncoding` (optional): skip per-component encoding for inserted params.\n\n```ts\nexport type ParamKeys =\n  | 'pathname'\n  | 'search'\n  | 'hash'\n  | 'username'\n  | 'password'\n  | 'protocol'\n  | 'hostname'\n  | 'port';\n\nexport interface ParamValues {\n  stringify?: (value: unknown) => string;\n  disableEncoding?: boolean;\n  groups: Record<string | number, unknown>;\n}\n\nexport type Params = Partial<Record<ParamKeys, ParamValues>>;\n```\n\n## How params map to patterns\n\n- Named params (e.g. `:id`) use `groups.id`.\n- Unnamed params (`*`, `(...)`) use numeric groups: `groups[0]`, `groups[1]`, etc.\n- If a component has no params, `groups[0]` can override the entire component.\n\nExample with positional groups:\n\n```ts\nconst pattern = new URLPattern({ pathname: '/files/*' });\nconst url = generate(pattern, {\n  protocol: { groups: { 0: 'https' } },\n  hostname: { groups: { 0: 'example.com' } },\n  pathname: { groups: { 0: 'docs/readme.md' } },\n});\n// https://example.com/files/docs/readme.md\n```\n\nExample with named groups:\n\n```ts\nconst pattern = new URLPattern('https://example.com/users/:id');\nconst url = generate(pattern, {\n  pathname: { groups: { id: 'alice' } },\n});\n// https://example.com/users/alice\n```\n\n## Encoding behavior\n\nEncoding is applied when values are inserted into the pattern, before the final `URL` object is built.\n\n- `pathname`: `encodeURIComponent`, preserving slashes for `+`, `*`, `(...)`, or `*` params. Existing percent-escapes are preserved.\n- `search`: URLSearchParams-style encoding (spaces become `+`).\n- `hash`: `encodeURIComponent`.\n- `protocol`, `hostname`, `port`, `username`, `password`: inserted verbatim, then normalized by the `URL` object.\n\n`disableEncoding` can be set per component to skip this pre-encoding. Note that the `URL` constructor and setters still normalize some characters (e.g. `?` and `#` in a pathname), so `disableEncoding` is not a bypass for URL parsing rules.\n\nExample: keep slashes in a path param while still encoding unsafe characters:\n\n```ts\nconst pattern = new URLPattern('https://example.com/:path+');\nconst url = generate(pattern, {\n  pathname: { groups: { path: 'foo/bar?baz' } },\n});\n// https://example.com/foo/bar%3Fbaz\n```\n\nExample: skip pre-encoding for one component:\n\n```ts\nconst pattern = new URLPattern('https://example.com/:path');\nconst url = generate(pattern, {\n  pathname: {\n    groups: { path: 'a/b?c#d' },\n    disableEncoding: true,\n  },\n});\n// https://example.com/a/b%3Fc%23d\n```\n\n## Search (query string) handling\n\nThere are two modes:\n\n1) Pattern-aware search (params in the search pattern)\n\n```ts\nconst pattern = new URLPattern('https://example.com/search?q=:q&limit=:limit');\nconst url = generate(pattern, {\n  search: { groups: { q: 'new shoes', limit: 20 } },\n});\n// https://example.com/search?q=new+shoes&limit=20\n```\n\n2) Wildcard or paramless search (search is `*` or contains no params)\n\n`groups[0]` is treated as a full search payload and can be:\n\n- a string (`\"q=1\"` or `\"?q=1\"`)\n- a `URLSearchParams`\n- an object (`{ q: 'new shoes', limit: 20 }`)\n- an array of tuples (`[['tag', 'a'], ['tag', 'b']]`)\n\n```ts\nconst pattern = new URLPattern({\n  protocol: 'https',\n  hostname: 'example.com',\n  pathname: '/search',\n  search: '*',\n});\n\nconst url = generate(pattern, {\n  search: { groups: { 0: { q: 'new shoes', limit: 20 } } },\n});\n// https://example.com/search?q=new+shoes&limit=20\n```\n\nTuple arrays are the easiest way to preserve repeated keys:\n\n```ts\nconst pattern = new URLPattern('https://example.com/search?*');\nconst url = generate(pattern, {\n  search: {\n    groups: { 0: [['tag', 'a'], ['tag', 'b']] },\n  },\n});\n// https://example.com/search?tag=a&tag=b\n```\n\n### Stringify behavior\n\nNon-string values are stringified with `String(value)` by default. Override per component with a `stringify` function:\n\n```ts\nconst pattern = new URLPattern('https://example.com/items/:id');\nconst url = generate(pattern, {\n  pathname: {\n    groups: { id: { nested: true } },\n    stringify: (value) => JSON.stringify(value),\n  },\n});\n// https://example.com/items/%7B%22nested%22%3Atrue%7D\n```\n\nFor `search: '*'` or paramless search, the stringifier is applied to each non-string value before passing into `URLSearchParams`.\n\n## Credentials and host components\n\n- When `protocol` and `hostname` are present, `username` and `password` are applied to the URL and encoded by the `URL` object.\n- If you provide `hostname` without `protocol`, the URL construction fails (invalid URL).\n- If you provide `protocol` without `hostname`, you get a scheme URL such as `myapp:foo`.\n\n```ts\nconst pattern = new URLPattern({ pathname: '/private' });\nconst url = generate(pattern, {\n  protocol: { groups: { 0: 'https' } },\n  hostname: { groups: { 0: 'example.com' } },\n  username: { groups: { 0: 'user name' } },\n  password: { groups: { 0: 'p@ss' } },\n});\n// https://user%20name:p%40ss@example.com/private\n```\n\n## Optional params and empty strings\n\n- `undefined`/`null` values are treated as missing.\n- Empty strings are considered provided and will keep optional prefixes (e.g. a trailing `/`).\n\n```ts\nconst pattern = new URLPattern('https://example.com/users/:id?');\nconst url = generate(pattern, {\n  pathname: { groups: { id: '' } },\n});\n// https://example.com/users/\n```\n\n## Edge cases and gotchas\n\n- Node's `URLPattern` can interpret certain escaped literals in patterns differently than browsers. Avoid relying on escapes for special characters in patterns unless you've verified behavior in your target environment.\n- `URL` normalization still applies even with `disableEncoding`.\n- Existing percent-escapes are preserved in pathname params; they are not preserved in search or hash params unless you disable encoding and provide the exact string you want.\n- If you want a host/authority URL, provide a protocol. Without one, the `URL` constructor interprets `host:port` as a scheme.\n- Relative URLs are not supported because `generate` always returns a `URL` object (absolute or scheme).\n\n## Intended scope\n\nThis package is a barebones extension of `URLPattern` itself: it only adds what is needed to generate URLs from patterns. It does not try to become a router, a URL serializer, or a standards wrapper. The goal is to keep `URLPattern` at the center and make it work in both directions.\n\n## Testing\n\nTest coverage is 100%, and runs in both node and the browser.\n\nThere is also a manual test page at `index.html` that exercises the generator in the browser.\n","readmeFilename":"README.md","_rev":"1-66c68d58b0b017326634440c816191d2"}