{"id":15414904,"url":"https://github.com/lxsmnsyc/silmaril","last_synced_at":"2025-04-19T12:48:23.662Z","repository":{"id":59667021,"uuid":"538164323","full_name":"lxsmnsyc/silmaril","owner":"lxsmnsyc","description":"Compile-time reactivity for JS","archived":false,"fork":false,"pushed_at":"2024-01-31T06:02:03.000Z","size":624,"stargazers_count":30,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-29T08:02:23.056Z","etag":null,"topics":["javascript","reactive-programming"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lxsmnsyc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-09-18T16:08:36.000Z","updated_at":"2024-07-06T17:10:17.000Z","dependencies_parsed_at":"2024-01-31T06:51:07.040Z","dependency_job_id":"b41a2c01-7be6-4e15-bd85-286297268215","html_url":"https://github.com/lxsmnsyc/silmaril","commit_stats":{"total_commits":26,"total_committers":1,"mean_commits":26.0,"dds":0.0,"last_synced_commit":"4ef98f41a9242d5ccba4c08e74305838cc72b497"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lxsmnsyc%2Fsilmaril","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lxsmnsyc%2Fsilmaril/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lxsmnsyc%2Fsilmaril/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lxsmnsyc%2Fsilmaril/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lxsmnsyc","download_url":"https://codeload.github.com/lxsmnsyc/silmaril/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249698761,"owners_count":21312263,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript","reactive-programming"],"created_at":"2024-10-01T17:05:13.854Z","updated_at":"2025-04-19T12:48:23.622Z","avatar_url":"https://github.com/lxsmnsyc.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# silmaril\n\n\u003e Compile-time reactivity for JS\n\n[![NPM](https://img.shields.io/npm/v/silmaril.svg)](https://www.npmjs.com/package/silmaril) [![JavaScript Style Guide](https://badgen.net/badge/code%20style/airbnb/ff5a5f?icon=airbnb)](https://github.com/airbnb/javascript) [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat-square\u0026logo=codesandbox)](https://codesandbox.io/s/github/LXSMNSYC/silmaril/tree/main/examples/demo)\n\n## Install\n\n```bash\nnpm install --save silmaril\n```\n\n```bash\nyarn add silmaril\n```\n\n```bash\npnpm add silmaril\n```\n\n## Features\n\n- Compile-time reactivity\n- Minimal reactive runtime\n- Auto-memoization\n- Stores\n\n## Requirement\n\nDue to the compile-time nature of this library, it requires the use of [Babel](https://babeljs.io/). `silmaril` provides a Babel plugin under `silmaril/babel`.\n\n## Usage\n\n### Basic reactivity\n\n`$$` defines the reactive boundary in your JS code. Any top-level variables (function-scoped) declared in `$$` will be treated as \"reactive\" as possible. `$` can be used to asynchronously react to variable changes.\n\nVariable changes and reactions are only limited in `$$` (even for nested `$$` calls).\n\n```js\nimport { $$, $ } from 'silmaril';\n\n$$(() =\u003e {\n  // Create a \"reactive\" variable\n  let count = 0;\n\n  // Log count for changes\n  $(console.log('Count: ', count));\n\n\n  function multiply() {\n    // Update count\n    count *= 100;\n  }\n\n  multiply();\n  // After some time, this code logs `Count: 100`.\n});\n```\n\n`$` will know which variables to track with, but it can only know if the variable is accessed in that same call.\n\n```js\nimport { $$, $ } from 'silmaril';\n\n$$(() =\u003e {\n  // Create a \"reactive\" variable\n  let count = 0;\n  let prefix = 'Count';\n\n  function log(current) {\n    // `prefix` is not tracked\n    console.log(`${prefix}: `, current);\n  }\n\n  // This only tracks `count`\n  $(log(count));\n});\n```\n\n`$` can also accept a function expression, and has the same tracking capabilities.\n\n```js\n$(() =\u003e {\n  // This tracks `count`\n  console.log('Count:', count);\n});\n```\n\n`$` will only run if the tracked variables have actually changed (except for the first run), which means that it has some \"auto-memoization\".\n\n### Computed variables\n\nIf a reactive variable references another, the variable becomes computed, which means that it will re-evaluate everytime the referenced variables changes.\n\n```js\nimport { $$, $ } from 'silmaril';\n\n$$(() =\u003e {\n  // Create a \"reactive\" variable\n  let count = 0;\n\n  // Create a \"reactive\" const variable.\n  const message = `Count: ${count}`;\n\n  // This only tracks `message`\n  $(console.log(message));\n\n  count = 100; // Logs 'Count: 100'\n});\n```\n\nUpdates on computed variables are synchronous.\n\n```js\nimport { $$ } from 'silmaril';\n\n$$(() =\u003e {\n  let count = 0;\n  const message = `Count: ${count}`;\n  count = 100; // message = Count: 100\n  count = 200; // message = Count: 200\n});\n```\n\nComputed variables are also writable if declared with `let`.\n\n```js\nimport { $$, $sync } from 'silmaril';\n\n$$(() =\u003e {\n  let count = 0;\n  let message = `Count: ${count}`;\n  $sync(console.log('Log', message)); // Log Count: 0\n  count = 100; // Log Count: 100\n  message = 'Hello World'; // Log Hello World\n  count = 200; // Log Count: 200\n});\n```\n\n### Lifecycles\n\n#### `onMount`\n\n`onMount` can be used to detect once `$$` has finished the setup.\n\n```js\nimport { $$, onMount } from 'silmaril';\n\n$$(() =\u003e {\n  onMount(() =\u003e {\n    console.log('Mounted!');\n  });\n  console.log('Not mounted yet!');\n});\n```\n\n`onMount` can also be used in `$`, `$sync`, `$composable` and computed variables.\n\n#### `onDestroy`\n\n`$$` returns a callback that allows disposing the reactive boundary. You can use `onDestroy` to detect when this happens.\n\n```js\nimport { $$, onDestroy } from 'silmaril';\n\nconst stop = $$(() =\u003e {\n  onDestroy(() =\u003e {\n    console.log('Destroyed!');\n  });\n});\n\n// ...\nstop();\n```\n\n`onDestroy` can also be used in `$`, `$sync`, `$composable` and computed variables.\n\n### Synchronous tracking\n\n`$` is deferred by a timeout schedule which means that `$` asynchronously reacts on variable updates, this is so that updates on variables are batched by default (writing multiple times synchronously will only cause a single asynchronous update).\n\n`$sync` provides synchronous tracking.\n\n```js\nimport { $$, $, $sync } from 'silmaril';\n\n$$(() =\u003e {\n  // Create a \"reactive\" variable\n  let count = 0;\n\n  // Create a \"reactive\" const variable.\n  const message = `Count: ${count}`;\n\n  $sync(console.log('Sync', message)); // Logs \"Sync Count: 0\"\n  $(console.log('Async', message));\n\n  count = 100; // Logs \"Sync Count: 100\"\n  count = 200; // Logs \"Sync Count: 200\"\n\n  // After some time the code ends, logs \"Async Count: 200\"\n});\n```\n\n### Stores\n\nReactivity is isolated in `$$`, but there are multiple ways to expose it outside `$$` e.g. emulating event emitters, using observables, global state management, etc.\n\n`silmaril/store` provides a simple API for this, and `$store` allows two-way (or one-way) binding for stores.\n\n```js\nimport { $$, $, $sync, $store } from 'silmaril';\nimport Store from 'silmaril/store';\n\n// Create a store\nconst count = new Store(100);\n\n// Subscribe to it\ncount.subscribe((current) =\u003e {\n  console.log('Raw Count:', current);\n});\n\n$$(() =\u003e {\n  // Bind the store to a reactive variable\n  let current = $store(count);\n  // `const` can also be used as an alternative\n  // for enforcing one-way binding\n  \n  // Tracking the bound variable\n  $sync(console.log('Sync Count:', current));\n  $(console.log('Async Count:', current));\n\n  // Mutate the variable (also mutates the store)\n  current += 100;\n\n  // Logs\n  // Sync Count: 100\n  // Raw Count: 200\n  // Sync Count: 200\n  // Async Count: 200\n});\n```\n\n`$store` can accept any kind of implementation as long as it follows the following interface:\n\n- `subscribe(callback: Function): Function`: accepts a callback and returns a cleanup callback\n- `get()`: returns the current state of the store\n- `set(state)`: optional, mutates the state of the store.\n\n### Composition\n\n#### `$composable`\n\n`$composable` allows composing functions that can be used in `$$`, `$sync`, `$`, another `$composable` or computed variables.\n\n```js\nimport { $$, $sync, $composable, $store, onDestroy } from 'silmaril';\nimport Store from 'silmaril/store';\n\n// Create a composable\nconst useSquared = $composable((store) =\u003e {\n  // Bind the input store to a variable\n  const input = $store(store);\n\n  // Create a store\n  const squaredStore = new Store(0);\n\n  // Make sure to cleanup the store\n  onDestroy(() =\u003e squaredStore.destroy());\n\n  // Update the store based on the bound input store\n  $sync(squaredStore.set(input ** 2));\n\n  // Return the store\n  return squaredStore;\n});\n\n$$(() =\u003e {\n  // Create a store\n  const store = new Store(0);\n\n  // Bind it\n  let input = $store(store);\n\n  // Track the value of the store\n  $sync(console.log('Value', input));\n\n  // Create a \"squared\" store based on the input store\n  // then bind it\n  const squared = $store(useSquared(store));\n\n  // Track the squared store\n  $sync(console.log('Squared', squared));\n\n  // Update the input store\n  input = 100;\n\n  // Logs\n  // Count: 0\n  // Count: 100\n  // Count: 200\n});\n```\n\n#### `$` and `$sync`\n\nBoth `$` and `$sync` behave much like `$$`: variables become reactive, `onMount` and `onDestroy` can be used, same goes to other APIs.\n\n```js\nimport { $$, $, onDestroy } from 'silmaril';\n\n$$(() =\u003e {\n  let y = 0;\n  $(() =\u003e {\n    let x = 0;\n\n    $(console.log(x + y));\n\n    onDestroy(() =\u003e {\n      console.log('This will be cleaned up when `y` changes');\n    });\n\n    x += 100;\n  });\n  y += 100;\n});\n```\n\n## Inspirations/Prior Art\n\n- [Svelte](https://svelte.dev/)\n- [Malina](https://malinajs.github.io/docs/)\n- [`solid-labels`](https://github.com/LXSMNSYC/solid-labels)\n- [Vue's Reactivity Transform](https://github.com/vuejs/rfcs/discussions/369)\n\n## Sponsors\n\n![Sponsors](https://github.com/lxsmnsyc/sponsors/blob/main/sponsors.svg?raw=true)\n\n## License\n\nMIT © [lxsmnsyc](https://github.com/lxsmnsyc)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flxsmnsyc%2Fsilmaril","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flxsmnsyc%2Fsilmaril","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flxsmnsyc%2Fsilmaril/lists"}