{"_id":"@dmytromykhailiuk/preact-signal-feature-query-param","_rev":"2-bd97f25a733d38d6aa9054ae78a4dd9b","name":"@dmytromykhailiuk/preact-signal-feature-query-param","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@dmytromykhailiuk/preact-signal-feature-query-param","version":"1.0.0","keywords":["preact","preact-signals","signals","feature-flag","feature-flags","feature-toggle","query-param","search-params","url","localstorage","zero-rerender","typescript","typed","type-safe"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/preact-signal-feature-query-param@1.0.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/preact-signal-feature-query-param#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-feature-query-param/issues"},"dist":{"shasum":"dc2701cf019ef0c50e4c7c897110368173ff4784","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/preact-signal-feature-query-param/-/preact-signal-feature-query-param-1.0.0.tgz","fileCount":9,"integrity":"sha512-lJVpAV6RvQqJqyAZ5P8jpvHqb4etMda1soqg4ohOhxjZq5p52IArj3MVO2KlYRZ+4uzYVvpmv1Vg3QXEmcECdA==","signatures":[{"sig":"MEUCIQCeJwpMxPNm6TX/K5txx4VNF+b6YzA2jlM1pvEalBpctwIgJ6VBX9fyxzZt6nqEp06pa3NUOvEmWG25sdqde39IKVw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":42324},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"b9a3a6389ad9ee20640e720823a33b05c357ce6f","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"vite --config vite.playground.config.ts","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/preact-signal-feature-query-param.git","type":"git"},"_npmVersion":"11.6.2","description":"Feature flags driven by a URL query param, persisted in localStorage and exposed as a typed Preact signal — validated, mapped, zero re-render.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","dependencies":{"@dmytromykhailiuk/preact-signal-utils":"^1.0.0","@dmytromykhailiuk/typed-local-storage":"^1.0.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vite":"^5.4.11","jsdom":"^25.0.1","preact":"^10.25.4","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4","@preact/signals":"^2.0.1","@preact/preset-vite":"^2.10.1","@testing-library/preact":"^3.2.4"},"peerDependencies":{"preact":">=10.11.0","@preact/signals":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/preact-signal-feature-query-param_1.0.0_1785227207473_0.6730281832465128","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@dmytromykhailiuk/preact-signal-feature-query-param","version":"1.0.1","description":"Feature flags driven by a URL query param, persisted in localStorage and exposed as a typed Preact signal — validated, mapped, zero re-render.","type":"module","sideEffects":false,"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","keywords":["preact","preact-signals","signals","feature-flag","feature-flags","feature-toggle","query-param","search-params","url","localstorage","zero-rerender","typescript","typed","type-safe"],"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","playground":"vite --config vite.playground.config.ts","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","lint":"biome check .","lint:fix":"biome check --write .","format":"biome format --write .","prepublishOnly":"npm run build"},"engines":{"node":">=18"},"dependencies":{"@dmytromykhailiuk/preact-signal-utils":"^1.0.0","@dmytromykhailiuk/typed-local-storage":"^1.0.0"},"peerDependencies":{"@preact/signals":"^2.0.0","preact":">=10.11.0"},"devDependencies":{"@biomejs/biome":"^1.9.4","@preact/preset-vite":"^2.10.1","@preact/signals":"^2.0.1","@testing-library/preact":"^3.2.4","@types/node":"^22.10.5","jsdom":"^25.0.1","preact":"^10.25.4","tsup":"^8.3.5","typescript":"^5.7.3","vite":"^5.4.11","vitest":"^2.1.8"},"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/preact-signal-feature-query-param.git"},"bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-feature-query-param/issues"},"homepage":"https://dmytromykhailiuk.github.io/preact-signal-feature-query-param/","gitHead":"f1ebce387bb4f918d30b4d0be6d35c0ed4365a8b","_id":"@dmytromykhailiuk/preact-signal-feature-query-param@1.0.1","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-rko/4Yy5/6BtZ2VlaxYQIrntt6X0CUfwIPU/9tWo3G5qORKijYTk21disyseBHpNK7JBtjsNBs4UdSAZFsjDAw==","shasum":"1dde535fbd49dcc3f4faedb4061907de570af713","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/preact-signal-feature-query-param/-/preact-signal-feature-query-param-1.0.1.tgz","fileCount":9,"unpackedSize":42317,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEjG491i7qeCyDiywUBZFmVmW+liuCeKl6avNO/vBLgTAiAZkDEZ+1QqNIQqpRmdq1D6OdD7SPCG57MQhUMH+m8lOA=="}]},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"directories":{},"maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/preact-signal-feature-query-param_1.0.1_1786638930955_0.304580181038121"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-28T08:26:47.169Z","modified":"2026-08-13T16:35:31.275Z","1.0.0":"2026-07-28T08:26:47.605Z","1.0.1":"2026-08-13T16:35:31.112Z"},"bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-feature-query-param/issues"},"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","homepage":"https://dmytromykhailiuk.github.io/preact-signal-feature-query-param/","keywords":["preact","preact-signals","signals","feature-flag","feature-flags","feature-toggle","query-param","search-params","url","localstorage","zero-rerender","typescript","typed","type-safe"],"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/preact-signal-feature-query-param.git"},"description":"Feature flags driven by a URL query param, persisted in localStorage and exposed as a typed Preact signal — validated, mapped, zero re-render.","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"readme":"# @dmytromykhailiuk/preact-signal-feature-query-param\n\nFeature flags driven by a URL query param, persisted in `localStorage` and exposed as a typed\nPreact signal — validated, mapped, zero re-render.\n\n> **Full documentation:** open [Docs](https://dmytromykhailiuk.github.io/preact-signal-feature-query-param/)\n> in a browser — every option, with examples, a table of contents and cross-links. This README is\n> the short form.\n\n`?language=fr`, `?image-upload=true`, `?resolution=4k` — the oldest feature-flag mechanism there\nis, and the one QA, support and demos actually use. Written by hand it is always the same\nsprawl: read the param, decide whether the value is one you accept, remember it so the next page\nview doesn't lose it, coerce the string into something the app can use, and give the rest of the\ncodebase a reactive handle on it — per flag, and never quite the same way twice.\n\nThis library is that sprawl, done once. One call declares a flag; what comes back is a\n`ReadonlySignal` you bind straight into JSX.\n\n## Install\n\n```sh\nnpm i @dmytromykhailiuk/preact-signal-feature-query-param\n```\n\nPeers: `@preact/signals` ≥ 2 and `preact` ≥ 10.11.\n\n## Quick start\n\n```ts\n// features.ts — the one place flags are declared\nimport { createFeatureQueryParam } from \"@dmytromykhailiuk/preact-signal-feature-query-param\";\n\nexport const language = createFeatureQueryParam(\"language\", {\n  availableValues: [\"en\", \"pt\", \"fr\"],\n  defaultValue: \"en\",\n});\n\nexport const imageUpload = createFeatureQueryParam<\"true\" | \"false\", boolean>(\"image-upload\", {\n  availableValues: [\"true\", \"false\"],\n  defaultValue: \"false\",\n  valueMapper: (value) => value === \"true\",\n});\n```\n\n```ts\n// main.ts — resolve every flag once, before the app renders\nimport { imageUpload, language } from \"./features\";\n\nlanguage.init();\nimageUpload.init();\n```\n\n```tsx\n// anywhere in the app — bind the signal, never unwrap it in render\nimport { Show } from \"@preact/signals/utils\";\nimport { imageUpload, language } from \"./features\";\n\nfunction Toolbar() {\n  return (\n    <>\n      <span>{language.signal$}</span>\n      <Show when={imageUpload.signal$}>\n        <UploadButton />\n      </Show>\n    </>\n  );\n}\n```\n\nOpen `?language=fr&image-upload=true` once, and both flags stick — the values are persisted, so\nthey survive the next reload with a clean address bar.\n\n## How a value is resolved\n\n`init()` looks in three places, in order, and takes the first **valid** value it finds:\n\n| source | wins when | persisted? |\n| --- | --- | --- |\n| the query param | it is present and valid | yes — it is an explicit, shareable choice |\n| `localStorage` | the URL carried nothing usable | already there |\n| `defaultValue` | nothing else was found | no |\n\nTwo consequences worth knowing:\n\n- **An invalid value never lands anywhere.** A param outside `availableValues`, or one the\n  `validator` turns down, is ignored — resolution simply falls through to the next source.\n- **The default is never written.** A flag nobody chose stays unset in storage, so changing\n  `defaultValue` in a later release still reaches returning users.\n\n`init({ withReset: true })` skips the stored value and drops it — \"start from the default unless\nthis URL says otherwise\". `init({ search })` takes the query string from anywhere else: a\nrequest URL on the server, a hash for hash-based routers, a router's own location.\n\n## API\n\n```ts\nconst feature = createFeatureQueryParam<T, K>(queryParamName, {\n  defaultValue: T;                  // used until something valid shows up; must itself be valid\n  availableValues?: readonly T[];   // the accepted set\n  validator?: (value: T) => boolean;// extra check; both must pass\n  valueMapper?: (value: T) => K;    // \"true\" → true, \"4k\" → { width, height }\n  persist?: boolean;                // default true\n  storageKey?: string;              // defaults to the query param name\n});\n\nfeature.signal$;          // ReadonlySignal<K> — the mapped value\nfeature.raw$;             // ReadonlySignal<T> — the raw string behind it\nfeature.peek();           // K, read without subscribing\nfeature.init(options?);   // resolve: URL → storage → default\nfeature.afterInit();      // Promise<void> — resolves once init() has run\nfeature.isInitialized();  // boolean\nfeature.update(value);    // set at runtime; false when the value was rejected\nfeature.reset();          // back to defaultValue, persisted value dropped\nfeature.isValid(value);   // type guard over availableValues + validator\nfeature.queryParamName;\nfeature.storageKey;       // null when persist is false\nfeature.defaultValue;\n```\n\nThe returned object is frozen, and every flag owns its storage key — declaring the same key twice\nthrows, the same way two owners of one `localStorage` key always were a bug.\n\n## Typing\n\n`T` is the raw string type; `K` is what the app consumes. Give one type argument and the flag is\nits own value; give two and `valueMapper` becomes required, because it is the only thing that can\nproduce a `K`:\n\n```ts\ncreateFeatureQueryParam(\"language\", { availableValues: [\"en\", \"fr\"], defaultValue: \"en\" });\n// FeatureQueryParam<\"en\" | \"fr\">  — inferred, no type arguments needed\n\ncreateFeatureQueryParam<\"fullhd\" | \"4k\", Resolution>(\"resolution\", {\n  availableValues: [\"fullhd\", \"4k\"],\n  defaultValue: \"4k\",\n  valueMapper: toResolution, // required — the compiler asks for it\n});\n\ncreateFeatureQueryParam<string>(\"build\", {\n  defaultValue: \"stable\",\n  validator: (value) => /^[a-z]+$/.test(value), // open-ended: no fixed set to infer from\n});\n```\n\n## Zero re-render\n\n`signal$` is a `ReadonlySignal`, so a flag change updates the DOM node it is bound to and nothing\nelse — no component re-renders, whether the change came from `init()` or from `update()`. Bind it\ndirectly, derive with `computed` / `useComputed`, and branch with `<Show>`:\n\n```tsx\nconst badge = useComputed(() => `${resolution.signal$.value.width}p`);\n\n<span>{badge}</span>\n<Show when={imageUpload.signal$} fallback={<Placeholder />}>\n  <UploadButton />\n</Show>\n```\n\n## Timing\n\n`init()` is explicit — call it when the URL is known, which for most apps is the first line of\n`main.ts`. Anything that has to wait can:\n\n```ts\nawait Promise.all([language.afterInit(), imageUpload.afterInit()]);\n```\n\n`afterInit()` resolves on the exact write that finishes `init()` (and immediately if it already\nran), so nothing polls and nothing races. Before `init()`, a flag reads as its `defaultValue`.\n\n## SSR\n\nNo `window`, no `location`, no `localStorage` — none of it is required. Features can be declared\nat module scope in code that also runs on the server: `location` is read through a guard,\npersistence falls back to an in-process store, and the request URL can be handed in explicitly:\n\n```ts\nlanguage.init({ search: request.url });\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}