├── packages ├── rollup │ ├── pridepack.json │ ├── .eslintrc.cjs │ ├── tsconfig.json │ ├── tsconfig.eslint.json │ ├── README.md │ ├── LICENSE │ ├── src │ │ └── index.ts │ ├── .gitignore │ └── package.json ├── solid │ ├── pridepack.json │ ├── .eslintrc.cjs │ ├── tsconfig.json │ ├── tsconfig.eslint.json │ ├── src │ │ └── index.ts │ ├── LICENSE │ ├── README.md │ ├── package.json │ └── .gitignore ├── vite │ ├── pridepack.json │ ├── .eslintrc.cjs │ ├── tsconfig.json │ ├── tsconfig.eslint.json │ ├── README.md │ ├── LICENSE │ ├── src │ │ └── index.ts │ ├── .gitignore │ └── package.json └── silmaril │ ├── pridepack.json │ ├── example.js │ ├── .eslintrc.cjs │ ├── tsconfig.json │ ├── tsconfig.eslint.json │ ├── babel │ ├── unwrap-node.ts │ ├── env.d.ts │ ├── checks.ts │ └── index.ts │ ├── store │ └── index.ts │ ├── LICENSE │ ├── .gitignore │ ├── package.json │ ├── test │ ├── compiler.test.ts │ └── __snapshots__ │ │ └── compiler.test.ts.snap │ ├── src │ └── index.ts │ └── README.md ├── examples ├── demo │ ├── src │ │ ├── vite-env.d.ts │ │ ├── style.css │ │ └── main.ts │ ├── .gitignore │ ├── vite.config.ts │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── tsconfig.eslint.json │ ├── index.html │ ├── package.json │ └── favicon.svg └── solid │ ├── src │ ├── vite-env.d.ts │ ├── main.tsx │ ├── count.ts │ └── App.tsx │ ├── .gitignore │ ├── .eslintrc.js │ ├── vite.config.ts │ ├── index.html │ ├── tsconfig.json │ ├── tsconfig.eslint.json │ ├── package.json │ └── favicon.svg ├── pnpm-workspace.yaml ├── .eslintrc ├── package.json ├── lerna.json ├── LICENSE ├── .gitignore └── README.md /packages/rollup/pridepack.json: -------------------------------------------------------------------------------- 1 | { 2 | "target": "es2017" 3 | } -------------------------------------------------------------------------------- /packages/solid/pridepack.json: -------------------------------------------------------------------------------- 1 | { 2 | "target": "es2017" 3 | } -------------------------------------------------------------------------------- /packages/vite/pridepack.json: -------------------------------------------------------------------------------- 1 | { 2 | "target": "es2017" 3 | } -------------------------------------------------------------------------------- /examples/demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/**/*' 3 | - 'examples/**/*' -------------------------------------------------------------------------------- /examples/solid/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /examples/solid/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /packages/silmaril/pridepack.json: -------------------------------------------------------------------------------- 1 | { 2 | "target": "es2017", 3 | "entrypoints": { 4 | "./babel": "babel/index.ts", 5 | "./store": "store/index.ts", 6 | ".": "src/index.ts" 7 | } 8 | } -------------------------------------------------------------------------------- /examples/solid/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'solid-js/web'; 2 | import App from './App'; 3 | 4 | const app = document.getElementById('app'); 5 | 6 | if (app) { 7 | render(() => , app); 8 | } 9 | -------------------------------------------------------------------------------- /examples/solid/src/count.ts: -------------------------------------------------------------------------------- 1 | import { createSignal } from 'solid-js'; 2 | import Store from 'silmaril/store'; 3 | 4 | export const countStore = new Store(0); 5 | 6 | export const countSignal = createSignal(0); 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "project": [ 5 | "./tsconfig.eslint.json", 6 | "./packages/*/tsconfig.json" 7 | ] 8 | }, 9 | "extends": [ 10 | "lxsmnsyc/typescript" 11 | ] 12 | } -------------------------------------------------------------------------------- /examples/demo/src/style.css: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: Avenir, Helvetica, Arial, sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | text-align: center; 6 | color: #2c3e50; 7 | margin-top: 60px; 8 | } 9 | -------------------------------------------------------------------------------- /examples/demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import silmaril from 'vite-plugin-silmaril'; 2 | 3 | export default { 4 | plugins: [ 5 | silmaril({ 6 | filter: { 7 | include: 'src/**/*.ts', 8 | exclude: 'node_modules/**/*.{ts,js}', 9 | }, 10 | }), 11 | ], 12 | }; -------------------------------------------------------------------------------- /examples/solid/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "extends": [ 4 | "lxsmnsyc/typescript/solid" 5 | ], 6 | "parserOptions": { 7 | "project": "./tsconfig.eslint.json", 8 | "tsconfigRootDir": __dirname, 9 | }, 10 | "rules": { 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*", 6 | "examples/*" 7 | ], 8 | "devDependencies": { 9 | "eslint": "^8.36.0", 10 | "eslint-config-lxsmnsyc": "^0.5.1", 11 | "lerna": "^6.5.1", 12 | "typescript": "^4.9.5" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "useWorkspaces": true, 3 | "packages": [ 4 | "packages/*", 5 | "examples/*" 6 | ], 7 | "command": { 8 | "version": { 9 | "exact": true 10 | }, 11 | "publish": { 12 | "allowBranch": [ 13 | "main" 14 | ], 15 | "registry": "https://registry.npmjs.org/" 16 | } 17 | }, 18 | "version": "0.3.3" 19 | } 20 | -------------------------------------------------------------------------------- /packages/silmaril/example.js: -------------------------------------------------------------------------------- 1 | const code = ` 2 | import { $$, $, onDestroy } from 'silmaril'; 3 | 4 | $$(() => { 5 | let y = 0; 6 | $(() => { 7 | let x = 0; 8 | 9 | $(console.log(x + y)); 10 | 11 | onDestroy(() => { 12 | console.log('This will be cleaned up when \`y\` changes'); 13 | }); 14 | 15 | x += 100; 16 | }); 17 | y += 100; 18 | }); 19 | `; 20 | -------------------------------------------------------------------------------- /examples/demo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "extends": [ 4 | 'lxsmnsyc/typescript', 5 | ], 6 | "parserOptions": { 7 | "project": "./tsconfig.eslint.json", 8 | "tsconfigRootDir": __dirname, 9 | }, 10 | "rules": { 11 | "import/no-extraneous-dependencies": [ 12 | "error", { 13 | "devDependencies": ["**/*.test.tsx"] 14 | } 15 | ], 16 | }, 17 | }; -------------------------------------------------------------------------------- /examples/solid/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import solidPlugin from 'vite-plugin-solid'; 3 | import silmaril from 'vite-plugin-silmaril'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | solidPlugin(), 8 | silmaril({ 9 | filter: { 10 | include: 'src/**/*.{ts,tsx}', 11 | exclude: 'node_modules/**/*.{ts,tsx}', 12 | }, 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /examples/solid/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { $, $$ } from 'silmaril'; 2 | 3 | const increment = document.getElementById('increment')!; 4 | const decrement = document.getElementById('decrement')!; 5 | const count = document.getElementById('count')!; 6 | 7 | $$(() => { 8 | let value = 0; 9 | 10 | $(count.innerHTML = `Count: ${value}`); 11 | 12 | increment.onclick = () => { 13 | value += 1; 14 | }; 15 | decrement.onclick = () => { 16 | value -= 1; 17 | }; 18 | }); 19 | -------------------------------------------------------------------------------- /examples/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "noEmit": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true 15 | }, 16 | "include": ["./src"] 17 | } 18 | -------------------------------------------------------------------------------- /examples/demo/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "noEmit": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true 15 | }, 16 | "include": ["./src"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/rollup/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "extends": [ 4 | 'lxsmnsyc/typescript', 5 | ], 6 | "parserOptions": { 7 | "project": "./tsconfig.eslint.json", 8 | "tsconfigRootDir": __dirname, 9 | }, 10 | "rules": { 11 | "import/no-extraneous-dependencies": [ 12 | "error", { 13 | "devDependencies": ["**/*.test.tsx"] 14 | } 15 | ], 16 | "no-param-reassign": "off", 17 | "no-restricted-syntax": "off" 18 | } 19 | }; -------------------------------------------------------------------------------- /packages/solid/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "extends": [ 4 | 'lxsmnsyc/typescript', 5 | ], 6 | "parserOptions": { 7 | "project": "./tsconfig.eslint.json", 8 | "tsconfigRootDir": __dirname, 9 | }, 10 | "rules": { 11 | "import/no-extraneous-dependencies": [ 12 | "error", { 13 | "devDependencies": ["**/*.test.tsx"] 14 | } 15 | ], 16 | "no-param-reassign": "off", 17 | "no-restricted-syntax": "off" 18 | } 19 | }; -------------------------------------------------------------------------------- /packages/vite/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "extends": [ 4 | 'lxsmnsyc/typescript', 5 | ], 6 | "parserOptions": { 7 | "project": "./tsconfig.eslint.json", 8 | "tsconfigRootDir": __dirname, 9 | }, 10 | "rules": { 11 | "import/no-extraneous-dependencies": [ 12 | "error", { 13 | "devDependencies": ["**/*.test.tsx"] 14 | } 15 | ], 16 | "no-param-reassign": "off", 17 | "no-restricted-syntax": "off" 18 | } 19 | }; -------------------------------------------------------------------------------- /packages/silmaril/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "extends": [ 4 | 'lxsmnsyc/typescript', 5 | ], 6 | "parserOptions": { 7 | "project": "./tsconfig.eslint.json", 8 | "tsconfigRootDir": __dirname, 9 | }, 10 | "rules": { 11 | "import/no-extraneous-dependencies": [ 12 | "error", { 13 | "devDependencies": ["**/*.test.tsx"] 14 | } 15 | ], 16 | "no-param-reassign": "off", 17 | "no-restricted-syntax": "off" 18 | } 19 | }; -------------------------------------------------------------------------------- /examples/solid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "noEmit": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "jsx": "preserve", 16 | "jsxImportSource": "solid-js", 17 | }, 18 | "include": ["./src"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/solid/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "noEmit": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "jsx": "preserve", 16 | "jsxImportSource": "solid-js", 17 | }, 18 | "include": ["./src"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 |

12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.3", 3 | "name": "demo", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "silmaril": "0.3.3" 12 | }, 13 | "devDependencies": { 14 | "eslint": "^8.36.0", 15 | "eslint-config-lxsmnsyc": "^0.5.1", 16 | "typescript": "^4.9.5", 17 | "vite": "^4.2.0", 18 | "vite-plugin-silmaril": "0.3.3" 19 | }, 20 | "private": true, 21 | "publishConfig": { 22 | "access": "restricted" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/rollup/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["ESNext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "jsx": "react", 18 | "esModuleInterop": true, 19 | "target": "ES2017" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/rollup/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["ESNext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "jsx": "react", 18 | "esModuleInterop": true, 19 | "target": "ES2017" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/solid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "jsx": "react", 18 | "esModuleInterop": true, 19 | "target": "ES2017" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "jsx": "react", 18 | "esModuleInterop": true, 19 | "target": "ES2017" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/solid/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "jsx": "react", 18 | "esModuleInterop": true, 19 | "target": "ES2017" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/vite/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "jsx": "react", 18 | "esModuleInterop": true, 19 | "target": "ES2017" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/silmaril/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["babel", "src", "store", "test"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["ESNext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "jsx": "react", 18 | "esModuleInterop": true, 19 | "target": "ES2017" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/silmaril/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["babel", "src", "store", "test"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["ESNext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "jsx": "react", 18 | "esModuleInterop": true, 19 | "target": "ES2017" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/silmaril/babel/unwrap-node.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types'; 2 | import { isNestedExpression } from './checks'; 3 | 4 | type TypeCheck = 5 | K extends (node: t.Node) => node is (infer U extends t.Node) 6 | ? U 7 | : never; 8 | 9 | type TypeFilter = (node: t.Node) => boolean; 10 | 11 | export default function unwrapNode( 12 | node: t.Node, 13 | key: K, 14 | ): TypeCheck | undefined { 15 | if (key(node)) { 16 | return node as TypeCheck; 17 | } 18 | if (isNestedExpression(node)) { 19 | return unwrapNode(node.expression, key); 20 | } 21 | return undefined; 22 | } 23 | -------------------------------------------------------------------------------- /examples/solid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.3", 3 | "name": "solid-demo", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "silmaril": "0.3.3", 12 | "solid-js": "^1.5.5", 13 | "solid-silmaril": "0.3.3" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^8.36.0", 17 | "eslint-config-lxsmnsyc": "^0.5.1", 18 | "typescript": "^4.9.5", 19 | "vite": "^4.2.0", 20 | "vite-plugin-silmaril": "0.3.3", 21 | "vite-plugin-solid": "^2.6.1" 22 | }, 23 | "private": true, 24 | "publishConfig": { 25 | "access": "restricted" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/silmaril/store/index.ts: -------------------------------------------------------------------------------- 1 | export default class Store { 2 | private alive = true; 3 | 4 | private value: T; 5 | 6 | private listeners = new Set<(value: T) => void>(); 7 | 8 | constructor(value: T) { 9 | this.value = value; 10 | } 11 | 12 | get(): T { 13 | return this.value; 14 | } 15 | 16 | set(value: T) { 17 | if (this.alive && !Object.is(this.value, value)) { 18 | this.value = value; 19 | 20 | for (const listener of this.listeners.keys()) { 21 | if (this.alive) { 22 | listener(value); 23 | } 24 | } 25 | } 26 | } 27 | 28 | subscribe(callback: () => void): () => void { 29 | if (this.alive) { 30 | this.listeners.add(callback); 31 | } 32 | return () => { 33 | this.listeners.delete(callback); 34 | }; 35 | } 36 | 37 | destroy() { 38 | if (this.alive) { 39 | this.alive = false; 40 | this.listeners.clear(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/rollup/README.md: -------------------------------------------------------------------------------- 1 | # rollup-plugin-silmaril 2 | 3 | > Rollup plugin for [`silmaril`](https://github.com/lxsmnsyc/silmaril) 4 | 5 | [![NPM](https://img.shields.io/npm/v/rollup-plugin-silmaril.svg)](https://www.npmjs.com/package/rollup-plugin-silmaril) [![JavaScript Style Guide](https://badgen.net/badge/code%20style/airbnb/ff5a5f?icon=airbnb)](https://github.com/airbnb/javascript) 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm install --D rollup-plugin-silmaril 11 | ``` 12 | 13 | ```bash 14 | yarn add -D rollup-plugin-silmaril 15 | ``` 16 | 17 | ```bash 18 | pnpm add -D rollup-plugin-silmaril 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```js 24 | import silmaril from 'rollup-plugin-silmaril'; 25 | 26 | ///... 27 | silmaril({ 28 | filter: { 29 | include: 'src/**/*.ts', 30 | exclude: 'node_modules/**/*.{ts,js}', 31 | }, 32 | }) 33 | ``` 34 | 35 | ## Sponsors 36 | 37 | ![Sponsors](https://github.com/lxsmnsyc/sponsors/blob/main/sponsors.svg?raw=true) 38 | 39 | ## License 40 | 41 | MIT © [lxsmnsyc](https://github.com/lxsmnsyc) 42 | -------------------------------------------------------------------------------- /packages/solid/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'silmaril'; 2 | import { 3 | createEffect, 4 | createRoot, 5 | createSignal, 6 | on, 7 | onCleanup, 8 | Signal, 9 | untrack, 10 | } from 'solid-js'; 11 | 12 | export interface SignalStore extends Store { 13 | set(value: T): void; 14 | } 15 | 16 | export function fromSignal([get, set]: Signal): SignalStore { 17 | return { 18 | get() { 19 | return untrack(get); 20 | }, 21 | set(value: T) { 22 | set(() => value); 23 | }, 24 | subscribe(callback) { 25 | return createRoot((cleanup) => { 26 | createEffect(on(get, callback)); 27 | return cleanup; 28 | }); 29 | }, 30 | }; 31 | } 32 | 33 | export function fromStore(store: Store): Signal { 34 | const [get, set] = createSignal(store.get()); 35 | 36 | createEffect(() => { 37 | if (store.set) { 38 | store.set(get()); 39 | } 40 | }); 41 | 42 | onCleanup(store.subscribe(() => { 43 | set(() => store.get()); 44 | })); 45 | 46 | return [get, set]; 47 | } 48 | -------------------------------------------------------------------------------- /packages/vite/README.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-silmaril 2 | 3 | > Vite plugin for [`silmaril`](https://github.com/lxsmnsyc/silmaril) 4 | 5 | [![NPM](https://img.shields.io/npm/v/vite-plugin-silmaril.svg)](https://www.npmjs.com/package/vite-plugin-silmaril) [![JavaScript Style Guide](https://badgen.net/badge/code%20style/airbnb/ff5a5f?icon=airbnb)](https://github.com/airbnb/javascript) 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm install --D vite-plugin-silmaril 11 | ``` 12 | 13 | ```bash 14 | yarn add -D vite-plugin-silmaril 15 | ``` 16 | 17 | ```bash 18 | pnpm add -D vite-plugin-silmaril 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```js 24 | import silmaril from 'vite-plugin-silmaril'; 25 | 26 | ///... 27 | export default { 28 | plugins: [ 29 | silmaril({ 30 | filter: { 31 | include: 'src/**/*.ts', 32 | exclude: 'node_modules/**/*.{ts,js}', 33 | }, 34 | }) 35 | ] 36 | } 37 | ``` 38 | 39 | ## Sponsors 40 | 41 | ![Sponsors](https://github.com/lxsmnsyc/sponsors/blob/main/sponsors.svg?raw=true) 42 | 43 | ## License 44 | 45 | MIT © [lxsmnsyc](https://github.com/lxsmnsyc) 46 | -------------------------------------------------------------------------------- /packages/rollup/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2022 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/silmaril/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2022 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/solid/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2022 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/vite/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2022 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alexis Munsayac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/silmaril/babel/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@babel/helper-module-imports' { 2 | import { NodePath } from '@babel/traverse'; 3 | import * as t from '@babel/types'; 4 | 5 | interface ImportOptions { 6 | importedSource: string | null; 7 | importedType: 'es6' | 'commonjs'; 8 | importedInterop: 'babel' | 'node' | 'compiled' | 'uncompiled'; 9 | importingInterop: 'babel' | 'node'; 10 | ensureLiveReference: boolean; 11 | ensureNoContext: boolean; 12 | importPosition: 'before' | 'after'; 13 | nameHint: string; 14 | blockHoist: number; 15 | } 16 | 17 | export function addDefault( 18 | path: NodePath, 19 | importedSource: string, 20 | opts?: Partial 21 | ): t.Identifier; 22 | export function addNamed( 23 | path: NodePath, 24 | name: string, 25 | importedSource: string, 26 | opts?: Partial 27 | ): t.Identifier; 28 | export function addNamespace( 29 | path: NodePath, 30 | importedSource: string, 31 | opts?: Partial 32 | ): t.Identifier; 33 | export function addSideEffect( 34 | path: NodePath, 35 | importedSource: string, 36 | opts?: Partial 37 | ): t.Identifier; 38 | } 39 | -------------------------------------------------------------------------------- /packages/silmaril/babel/checks.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types'; 2 | import * as babel from '@babel/core'; 3 | 4 | export function getImportSpecifierName(specifier: t.ImportSpecifier): string { 5 | if (t.isIdentifier(specifier.imported)) { 6 | return specifier.imported.name; 7 | } 8 | return specifier.imported.value; 9 | } 10 | 11 | type TypeFilter = (node: t.Node) => node is V; 12 | 13 | export function isPathValid( 14 | path: unknown, 15 | key: TypeFilter, 16 | ): path is babel.NodePath { 17 | return key((path as babel.NodePath).node); 18 | } 19 | 20 | export type NestedExpression = 21 | | t.ParenthesizedExpression 22 | | t.TypeCastExpression 23 | | t.TSAsExpression 24 | | t.TSSatisfiesExpression 25 | | t.TSNonNullExpression 26 | | t.TSInstantiationExpression 27 | | t.TSTypeAssertion; 28 | 29 | export function isNestedExpression(node: t.Node): node is NestedExpression { 30 | return t.isParenthesizedExpression(node) 31 | || t.isTypeCastExpression(node) 32 | || t.isTSAsExpression(node) 33 | || t.isTSSatisfiesExpression(node) 34 | || t.isTSNonNullExpression(node) 35 | || t.isTSTypeAssertion(node) 36 | || t.isTSInstantiationExpression(node); 37 | } 38 | -------------------------------------------------------------------------------- /examples/solid/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | $$, 3 | $store, 4 | $, 5 | } from 'silmaril'; 6 | import { 7 | createEffect, 8 | JSX, 9 | onCleanup, 10 | } from 'solid-js'; 11 | import { fromSignal, fromStore } from 'solid-silmaril'; 12 | import { countSignal, countStore } from './count'; 13 | 14 | function createInterval(callback: () => void, timeout: number) { 15 | createEffect(() => { 16 | const t = setInterval(callback, timeout); 17 | 18 | onCleanup(() => { 19 | clearInterval(t); 20 | }); 21 | }); 22 | } 23 | 24 | function CountFromStore() { 25 | const [count, setCount] = fromStore(countStore); 26 | 27 | createInterval(() => { 28 | setCount((current) => current + 1); 29 | }, 1000); 30 | 31 | return

{`fromStore: ${count()}`}

; 32 | } 33 | 34 | function CountFromSignal() { 35 | const store = fromSignal(countSignal); 36 | 37 | createInterval(() => { 38 | store.set?.(store.get() + 1); 39 | }, 1000); 40 | 41 | let ref: HTMLHeadingElement | undefined; 42 | 43 | onCleanup($$(() => { 44 | const value = $store(store); 45 | 46 | $(ref!.innerText = `fromSignal: ${value}`); 47 | })); 48 | 49 | return

{`fromSignal: ${store.get()}`}

; 50 | } 51 | 52 | export default function App(): JSX.Element { 53 | return ( 54 | <> 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /examples/demo/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/solid/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/solid/README.md: -------------------------------------------------------------------------------- 1 | # solid-silmaril 2 | 3 | > SolidJS bindings for [`silmaril`](https://github.com/lxsmnsyc/silmaril) 4 | 5 | [![NPM](https://img.shields.io/npm/v/solid-silmaril.svg)](https://www.npmjs.com/package/solid-silmaril) [![JavaScript Style Guide](https://badgen.net/badge/code%20style/airbnb/ff5a5f?icon=airbnb)](https://github.com/airbnb/javascript) 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm install silmaril solid-silmaril 11 | ``` 12 | 13 | ```bash 14 | yarn add silmaril solid-silmaril 15 | ``` 16 | 17 | ```bash 18 | pnpm add silmaril solid-silmaril 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### `fromSignal` 24 | 25 | ```js 26 | import { createSignal, createEffect } from 'solid-js'; 27 | import { fromSignal } from 'solid-silmaril'; 28 | import { $$, $sync, $store } from 'silmaril'; 29 | 30 | const [count, setCount] = createSignal(0); 31 | 32 | const countStore = fromSignal([count, setCount]); 33 | 34 | createEffect(() => { 35 | console.log('SolidJS Count:', count()); 36 | }); 37 | 38 | $$(() => { 39 | let counter = $store(countStore); 40 | 41 | $sync(console.log('Silmaril Count:', counter)); 42 | 43 | setInterval(() => { 44 | counter += 1; 45 | }, 1000); 46 | }); 47 | ``` 48 | 49 | ### `fromStore` 50 | 51 | ```js 52 | import { createEffect } from 'solid-js'; 53 | import { fromStore } from 'solid-silmaril'; 54 | import { $$, $sync, $store } from 'silmaril'; 55 | 56 | const countStore = new Store(0); 57 | 58 | const [count, setCount] = fromStore(countStore); 59 | 60 | $$(() => { 61 | let counter = $store(countStore); 62 | 63 | $sync(console.log('Silmaril Count:', counter)); 64 | }); 65 | 66 | createEffect(() => { 67 | console.log('SolidJS Count:', count()); 68 | }); 69 | 70 | setInterval(() => { 71 | setCount((current) += 1); 72 | }, 1000); 73 | ``` 74 | 75 | ## Sponsors 76 | 77 | ![Sponsors](https://github.com/lxsmnsyc/sponsors/blob/main/sponsors.svg?raw=true) 78 | 79 | ## License 80 | 81 | MIT © [lxsmnsyc](https://github.com/lxsmnsyc) 82 | -------------------------------------------------------------------------------- /packages/vite/src/index.ts: -------------------------------------------------------------------------------- 1 | import silmarilBabel from 'silmaril/babel'; 2 | import { Plugin } from 'vite'; 3 | import { createFilter, FilterPattern } from '@rollup/pluginutils'; 4 | import * as babel from '@babel/core'; 5 | import path from 'path'; 6 | 7 | export interface SilmarilPluginFilter { 8 | include?: FilterPattern; 9 | exclude?: FilterPattern; 10 | } 11 | 12 | export interface SilmarilPluginOptions { 13 | filter?: SilmarilPluginFilter; 14 | babel?: babel.TransformOptions; 15 | } 16 | 17 | export default function silmarilPlugin( 18 | options: SilmarilPluginOptions = {}, 19 | ): Plugin { 20 | const filter = createFilter( 21 | options.filter?.include, 22 | options.filter?.exclude, 23 | ); 24 | return { 25 | name: 'silmaril', 26 | async transform(code, id) { 27 | if (filter(id)) { 28 | const pluginOption = [silmarilBabel, {}]; 29 | const plugins: NonNullable['plugins']> = ['jsx']; 30 | if (/\.[mc]?tsx?$/i.test(id)) { 31 | plugins.push('typescript'); 32 | } 33 | const result = await babel.transformAsync(code, { 34 | ...options.babel, 35 | plugins: [ 36 | pluginOption, 37 | ...(options.babel?.plugins || []), 38 | ], 39 | parserOpts: { 40 | ...(options.babel?.parserOpts || {}), 41 | plugins: [ 42 | ...(options.babel?.parserOpts?.plugins || []), 43 | ...plugins, 44 | ], 45 | }, 46 | filename: path.basename(id), 47 | ast: false, 48 | sourceMaps: true, 49 | configFile: false, 50 | babelrc: false, 51 | sourceFileName: id, 52 | }); 53 | 54 | if (result) { 55 | return { 56 | code: result.code || '', 57 | map: result.map, 58 | }; 59 | } 60 | } 61 | return undefined; 62 | }, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /packages/rollup/src/index.ts: -------------------------------------------------------------------------------- 1 | import silmarilBabel from 'silmaril/babel'; 2 | import { Plugin } from 'rollup'; 3 | import { createFilter, FilterPattern } from '@rollup/pluginutils'; 4 | import * as babel from '@babel/core'; 5 | import path from 'path'; 6 | 7 | export interface SilmarilPluginFilter { 8 | include?: FilterPattern; 9 | exclude?: FilterPattern; 10 | } 11 | 12 | export interface SilmarilPluginOptions { 13 | filter?: SilmarilPluginFilter; 14 | babel?: babel.TransformOptions; 15 | } 16 | 17 | export default function silmarilPlugin( 18 | options: SilmarilPluginOptions = {}, 19 | ): Plugin { 20 | const filter = createFilter( 21 | options.filter?.include, 22 | options.filter?.exclude, 23 | ); 24 | return { 25 | name: 'silmaril', 26 | async transform(code, id) { 27 | if (filter(id)) { 28 | 29 | const pluginOption = [silmarilBabel, {}]; 30 | const plugins: NonNullable['plugins']> = ['jsx']; 31 | if (/\.[mc]?tsx?$/i.test(id)) { 32 | plugins.push('typescript'); 33 | } 34 | const result = await babel.transformAsync(code, { 35 | ...options.babel, 36 | plugins: [ 37 | pluginOption, 38 | ...(options.babel?.plugins || []), 39 | ], 40 | parserOpts: { 41 | ...(options.babel?.parserOpts || {}), 42 | plugins: [ 43 | ...(options.babel?.parserOpts?.plugins || []), 44 | ...plugins, 45 | ], 46 | }, 47 | filename: path.basename(id), 48 | ast: false, 49 | sourceMaps: true, 50 | configFile: false, 51 | babelrc: false, 52 | sourceFileName: id, 53 | }); 54 | 55 | if (result) { 56 | return { 57 | code: result.code || '', 58 | map: result.map, 59 | }; 60 | } 61 | } 62 | return undefined; 63 | }, 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /packages/solid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-silmaril", 3 | "version": "0.3.3", 4 | "type": "module", 5 | "files": [ 6 | "dist", 7 | "babel", 8 | "core" 9 | ], 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "license": "MIT", 14 | "keywords": [ 15 | "pridepack" 16 | ], 17 | "devDependencies": { 18 | "@types/node": "^18.15.3", 19 | "eslint": "^8.36.0", 20 | "eslint-config-lxsmnsyc": "^0.5.1", 21 | "pridepack": "2.4.2", 22 | "silmaril": "0.3.3", 23 | "solid-js": "^1.5.5", 24 | "tslib": "^2.5.0", 25 | "typescript": "^4.9.5", 26 | "vitest": "^0.29.2" 27 | }, 28 | "peerDependencies": { 29 | "silmaril": "^0.1", 30 | "solid-js": "^1.5" 31 | }, 32 | "scripts": { 33 | "prepublishOnly": "pridepack clean && pridepack build", 34 | "build": "pridepack build", 35 | "type-check": "pridepack check", 36 | "lint": "pridepack lint", 37 | "clean": "pridepack clean", 38 | "watch": "pridepack watch", 39 | "start": "pridepack start", 40 | "dev": "pridepack dev", 41 | "test": "vitest" 42 | }, 43 | "description": "Compile-time reactivity for JS", 44 | "repository": { 45 | "url": "https://github.com/lxsmnsyc/silmaril.git", 46 | "type": "git" 47 | }, 48 | "homepage": "https://github.com/lxsmnsyc/silmaril/packages/rollup", 49 | "bugs": { 50 | "url": "https://github.com/lxsmnsyc/silmaril/issues" 51 | }, 52 | "publishConfig": { 53 | "access": "public" 54 | }, 55 | "author": "Alexis Munsayac", 56 | "private": false, 57 | "types": "./dist/types/index.d.ts", 58 | "main": "./dist/cjs/production/index.cjs", 59 | "module": "./dist/esm/production/index.mjs", 60 | "exports": { 61 | ".": { 62 | "development": { 63 | "require": "./dist/cjs/development/index.cjs", 64 | "import": "./dist/esm/development/index.mjs" 65 | }, 66 | "require": "./dist/cjs/production/index.cjs", 67 | "import": "./dist/esm/production/index.mjs", 68 | "types": "./dist/types/index.d.ts" 69 | } 70 | }, 71 | "typesVersions": { 72 | "*": {} 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/rollup/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.production 74 | .env.development 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | .npmrc 108 | -------------------------------------------------------------------------------- /packages/silmaril/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.production 74 | .env.development 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | .npmrc 108 | -------------------------------------------------------------------------------- /packages/solid/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.production 74 | .env.development 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | .npmrc 108 | -------------------------------------------------------------------------------- /packages/vite/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.production 74 | .env.development 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | .npmrc 108 | -------------------------------------------------------------------------------- /packages/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-silmaril", 3 | "version": "0.3.3", 4 | "type": "module", 5 | "files": [ 6 | "dist", 7 | "babel", 8 | "core" 9 | ], 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "license": "MIT", 14 | "keywords": [ 15 | "pridepack" 16 | ], 17 | "devDependencies": { 18 | "@types/babel__core": "^7.20.0", 19 | "@types/node": "^18.15.3", 20 | "eslint": "^8.36.0", 21 | "eslint-config-lxsmnsyc": "^0.5.1", 22 | "pridepack": "2.4.2", 23 | "silmaril": "0.3.3", 24 | "tslib": "^2.5.0", 25 | "typescript": "^4.9.5", 26 | "vite": "^4.2.0", 27 | "vitest": "^0.29.2" 28 | }, 29 | "dependencies": { 30 | "@babel/core": "^7.21.0", 31 | "@rollup/pluginutils": "^5.0.2" 32 | }, 33 | "peerDependencies": { 34 | "silmaril": "^0.1", 35 | "vite": "^3 || ^4" 36 | }, 37 | "scripts": { 38 | "prepublishOnly": "pridepack clean && pridepack build", 39 | "build": "pridepack build", 40 | "type-check": "pridepack check", 41 | "lint": "pridepack lint", 42 | "clean": "pridepack clean", 43 | "watch": "pridepack watch", 44 | "start": "pridepack start", 45 | "dev": "pridepack dev", 46 | "test": "vitest" 47 | }, 48 | "description": "Compile-time reactivity for JS", 49 | "repository": { 50 | "url": "https://github.com/lxsmnsyc/silmaril.git", 51 | "type": "git" 52 | }, 53 | "homepage": "https://github.com/lxsmnsyc/silmaril/packages/rollup", 54 | "bugs": { 55 | "url": "https://github.com/lxsmnsyc/silmaril/issues" 56 | }, 57 | "publishConfig": { 58 | "access": "public" 59 | }, 60 | "author": "Alexis Munsayac", 61 | "private": false, 62 | "types": "./dist/types/index.d.ts", 63 | "main": "./dist/cjs/production/index.cjs", 64 | "module": "./dist/esm/production/index.mjs", 65 | "exports": { 66 | ".": { 67 | "development": { 68 | "require": "./dist/cjs/development/index.cjs", 69 | "import": "./dist/esm/development/index.mjs" 70 | }, 71 | "require": "./dist/cjs/production/index.cjs", 72 | "import": "./dist/esm/production/index.mjs", 73 | "types": "./dist/types/index.d.ts" 74 | } 75 | }, 76 | "typesVersions": { 77 | "*": {} 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/rollup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollup-plugin-silmaril", 3 | "version": "0.3.3", 4 | "type": "module", 5 | "files": [ 6 | "dist", 7 | "babel", 8 | "core" 9 | ], 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "license": "MIT", 14 | "keywords": [ 15 | "pridepack" 16 | ], 17 | "devDependencies": { 18 | "@types/babel__core": "^7.20.0", 19 | "@types/node": "^18.15.3", 20 | "eslint": "^8.36.0", 21 | "eslint-config-lxsmnsyc": "^0.5.1", 22 | "pridepack": "2.4.2", 23 | "rollup": "^2.79.0", 24 | "silmaril": "0.3.3", 25 | "tslib": "^2.5.0", 26 | "typescript": "^4.9.5", 27 | "vitest": "^0.29.2" 28 | }, 29 | "dependencies": { 30 | "@babel/core": "^7.21.0", 31 | "@rollup/pluginutils": "^5.0.2" 32 | }, 33 | "peerDependencies": { 34 | "rollup": "^2", 35 | "silmaril": "^0.1" 36 | }, 37 | "scripts": { 38 | "prepublishOnly": "pridepack clean && pridepack build", 39 | "build": "pridepack build", 40 | "type-check": "pridepack check", 41 | "lint": "pridepack lint", 42 | "clean": "pridepack clean", 43 | "watch": "pridepack watch", 44 | "start": "pridepack start", 45 | "dev": "pridepack dev", 46 | "test": "vitest" 47 | }, 48 | "description": "Compile-time reactivity for JS", 49 | "repository": { 50 | "url": "https://github.com/lxsmnsyc/silmaril.git", 51 | "type": "git" 52 | }, 53 | "homepage": "https://github.com/lxsmnsyc/silmaril/packages/rollup", 54 | "bugs": { 55 | "url": "https://github.com/lxsmnsyc/silmaril/issues" 56 | }, 57 | "publishConfig": { 58 | "access": "public" 59 | }, 60 | "author": "Alexis Munsayac", 61 | "private": false, 62 | "types": "./dist/types/index.d.ts", 63 | "main": "./dist/cjs/production/index.cjs", 64 | "module": "./dist/esm/production/index.mjs", 65 | "exports": { 66 | ".": { 67 | "development": { 68 | "require": "./dist/cjs/development/index.cjs", 69 | "import": "./dist/esm/development/index.mjs" 70 | }, 71 | "require": "./dist/cjs/production/index.cjs", 72 | "import": "./dist/esm/production/index.mjs", 73 | "types": "./dist/types/index.d.ts" 74 | } 75 | }, 76 | "typesVersions": { 77 | "*": {} 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.production 74 | .env.development 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | .npmrc 108 | .poneglyph 109 | 110 | .rigidity 111 | 112 | .vercel 113 | -------------------------------------------------------------------------------- /packages/silmaril/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "silmaril", 3 | "version": "0.3.3", 4 | "type": "module", 5 | "files": [ 6 | "dist", 7 | "babel", 8 | "core" 9 | ], 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "license": "MIT", 14 | "keywords": [ 15 | "pridepack" 16 | ], 17 | "devDependencies": { 18 | "@babel/core": "^7.21.0", 19 | "@types/babel__core": "^7.20.0", 20 | "@types/babel__traverse": "^7.18.1", 21 | "@types/node": "^18.15.3", 22 | "eslint": "^8.36.0", 23 | "eslint-config-lxsmnsyc": "^0.5.1", 24 | "pridepack": "2.4.2", 25 | "tslib": "^2.5.0", 26 | "typescript": "^4.9.5", 27 | "vitest": "^0.29.2" 28 | }, 29 | "peerDependencies": { 30 | "@babel/core": "^7.19" 31 | }, 32 | "scripts": { 33 | "prepublishOnly": "pridepack clean && pridepack build", 34 | "build": "pridepack build", 35 | "type-check": "pridepack check", 36 | "lint": "pridepack lint", 37 | "clean": "pridepack clean", 38 | "watch": "pridepack watch", 39 | "start": "pridepack start", 40 | "dev": "pridepack dev", 41 | "test": "vitest" 42 | }, 43 | "description": "Compile-time reactivity for JS", 44 | "repository": { 45 | "url": "https://github.com/lxsmnsyc/silmaril.git", 46 | "type": "git" 47 | }, 48 | "homepage": "https://github.com/lxsmnsyc/silmaril/packages/silmaril", 49 | "bugs": { 50 | "url": "https://github.com/lxsmnsyc/silmaril/issues" 51 | }, 52 | "publishConfig": { 53 | "access": "public" 54 | }, 55 | "author": "Alexis Munsayac", 56 | "private": false, 57 | "dependencies": { 58 | "@babel/helper-module-imports": "^7.18.6", 59 | "@babel/traverse": "^7.21.2", 60 | "@babel/types": "^7.21.2" 61 | }, 62 | "types": "./dist/types/src/index.d.ts", 63 | "main": "./dist/cjs/production/index.cjs", 64 | "module": "./dist/esm/production/index.mjs", 65 | "exports": { 66 | "./babel": { 67 | "development": { 68 | "require": "./dist/cjs/development/babel.cjs", 69 | "import": "./dist/esm/development/babel.mjs" 70 | }, 71 | "require": "./dist/cjs/production/babel.cjs", 72 | "import": "./dist/esm/production/babel.mjs", 73 | "types": "./dist/types/babel/index.d.ts" 74 | }, 75 | "./store": { 76 | "development": { 77 | "require": "./dist/cjs/development/store.cjs", 78 | "import": "./dist/esm/development/store.mjs" 79 | }, 80 | "require": "./dist/cjs/production/store.cjs", 81 | "import": "./dist/esm/production/store.mjs", 82 | "types": "./dist/types/store/index.d.ts" 83 | }, 84 | ".": { 85 | "development": { 86 | "require": "./dist/cjs/development/index.cjs", 87 | "import": "./dist/esm/development/index.mjs" 88 | }, 89 | "require": "./dist/cjs/production/index.cjs", 90 | "import": "./dist/esm/production/index.mjs", 91 | "types": "./dist/types/src/index.d.ts" 92 | } 93 | }, 94 | "typesVersions": { 95 | "*": { 96 | "babel": [ 97 | "./dist/types/babel/index.d.ts" 98 | ], 99 | "store": [ 100 | "./dist/types/store/index.d.ts" 101 | ] 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/silmaril/test/compiler.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as babel from '@babel/core'; 3 | import plugin from '../babel'; 4 | 5 | async function compile(code: string) { 6 | const result = await babel.transformAsync(code, { 7 | plugins: [ 8 | [plugin], 9 | ], 10 | }); 11 | return result?.code; 12 | } 13 | 14 | describe('let', () => { 15 | it('should compile on AssignmentExpression', async () => { 16 | const result = await compile(` 17 | import { $$ } from 'silmaril'; 18 | 19 | $$(() => { 20 | let x = 0; 21 | 22 | function increment() { 23 | x += 1; 24 | } 25 | }); 26 | `); 27 | 28 | expect(result).toMatchSnapshot(); 29 | }); 30 | it('should compile on UpdateExpression', async () => { 31 | const result = await compile(` 32 | import { $$ } from 'silmaril'; 33 | 34 | $$(() => { 35 | let x = 0; 36 | 37 | function increment() { 38 | x++; 39 | } 40 | }); 41 | `); 42 | 43 | expect(result).toMatchSnapshot(); 44 | }); 45 | it('should compile when accessing owned variables', async () => { 46 | const result = await compile(` 47 | import { $$ } from 'silmaril'; 48 | 49 | $$(() => { 50 | let greeting = 'Hello'; 51 | let receiver = 'World'; 52 | const message = greeting + ' ' + receiver; 53 | }); 54 | `); 55 | 56 | expect(result).toMatchSnapshot(); 57 | }); 58 | it('should not compile when not accessing owned variables', async () => { 59 | const result = await compile(` 60 | import { $$ } from 'silmaril'; 61 | 62 | $$(() => { 63 | const message = Math.random(); 64 | }); 65 | `); 66 | 67 | expect(result).toMatchSnapshot(); 68 | }); 69 | }); 70 | describe('$', () => { 71 | it('should compile to $$effect', async () => { 72 | const result = await compile(` 73 | import { $$, $ } from 'silmaril'; 74 | 75 | $$(() => { 76 | $(console.log('Example')); 77 | $(() => console.log('Example')); 78 | }); 79 | `); 80 | 81 | expect(result).toMatchSnapshot(); 82 | }); 83 | it('should subscribe to owned variables', async () => { 84 | const result = await compile(` 85 | import { $$, $ } from 'silmaril'; 86 | 87 | $$(() => { 88 | let x = 0; 89 | $(console.log(x)); 90 | $(() => console.log(x)); 91 | }); 92 | `); 93 | 94 | expect(result).toMatchSnapshot(); 95 | }); 96 | it('should not subscribe to unowned variables', async () => { 97 | const result = await compile(` 98 | import { $$, $ } from 'silmaril'; 99 | 100 | let x = 0; 101 | $$(() => { 102 | $(console.log(x)); 103 | $(() => console.log(x)); 104 | }); 105 | `); 106 | 107 | expect(result).toMatchSnapshot(); 108 | }); 109 | }); 110 | describe('$sync', () => { 111 | it('should compile to $$sync', async () => { 112 | const result = await compile(` 113 | import { $$, $sync } from 'silmaril'; 114 | 115 | $$(() => { 116 | $sync(console.log('Example')); 117 | $sync(() => console.log('Example')); 118 | }); 119 | `); 120 | 121 | expect(result).toMatchSnapshot(); 122 | }); 123 | it('should subscribe to owned variables', async () => { 124 | const result = await compile(` 125 | import { $$, $sync } from 'silmaril'; 126 | 127 | $$(() => { 128 | let x = 0; 129 | $sync(console.log(x)); 130 | $sync(() => console.log(x)); 131 | }); 132 | `); 133 | 134 | expect(result).toMatchSnapshot(); 135 | }); 136 | it('should not subscribe to unowned variables', async () => { 137 | const result = await compile(` 138 | import { $$, $sync } from 'silmaril'; 139 | 140 | let x = 0; 141 | $$(() => { 142 | $sync(console.log(x)); 143 | $sync(() => console.log(x)); 144 | }); 145 | `); 146 | 147 | expect(result).toMatchSnapshot(); 148 | }); 149 | }); 150 | describe('onMount', () => { 151 | it('should compile to $$mount', async () => { 152 | const result = await compile(` 153 | import { $$, onMount } from 'silmaril'; 154 | 155 | $$(() => { 156 | onMount(() => sconsole.log('Example')); 157 | }); 158 | `); 159 | 160 | expect(result).toMatchSnapshot(); 161 | }); 162 | }); 163 | describe('onDestroy', () => { 164 | it('should compile to $$destroy', async () => { 165 | const result = await compile(` 166 | import { $$, onDestroy } from 'silmaril'; 167 | 168 | $$(() => { 169 | onDestroy(() => console.log('Example')); 170 | }); 171 | `); 172 | 173 | expect(result).toMatchSnapshot(); 174 | }); 175 | }); 176 | describe('$store', () => { 177 | it('should compile $store with const', async () => { 178 | const result = await compile(` 179 | import { $$, $store } from 'silmaril'; 180 | 181 | $$(() => { 182 | const example = $store(someStore); 183 | }); 184 | `); 185 | 186 | expect(result).toMatchSnapshot(); 187 | }); 188 | it('should compile $store with let', async () => { 189 | const result = await compile(` 190 | import { $$, $store } from 'silmaril'; 191 | 192 | $$(() => { 193 | let example = $store(someStore); 194 | }); 195 | `); 196 | 197 | expect(result).toMatchSnapshot(); 198 | }); 199 | it('should allow updates for mutable stores', async () => { 200 | const result = await compile(` 201 | import { $$, $store } from 'silmaril'; 202 | 203 | $$(() => { 204 | let example = $store(someStore); 205 | 206 | function mutate() { 207 | example = newValue; 208 | } 209 | }); 210 | `); 211 | 212 | expect(result).toMatchSnapshot(); 213 | }); 214 | it('should allow tracking for stores', async () => { 215 | const result = await compile(` 216 | import { $$, $store } from 'silmaril'; 217 | 218 | $$(() => { 219 | let example = $store(someStore); 220 | 221 | $(console.log(example)); 222 | }); 223 | `); 224 | 225 | expect(result).toMatchSnapshot(); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /packages/silmaril/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | function forEach(arr: T[], cb: (item: T) => void) { 3 | for (let i = 0, len = arr.length; i < len; i += 1) { 4 | cb(arr[i]); 5 | } 6 | } 7 | 8 | interface Instance { 9 | alive: boolean; 10 | mounted: boolean; 11 | signals: any[][]; 12 | timeout?: ReturnType; 13 | instances: Instance[]; 14 | syncs: (() => void)[]; 15 | effects: (() => void)[]; 16 | mounts: (() => void)[]; 17 | destroys: (() => void)[]; 18 | count: number; 19 | } 20 | 21 | let CURRENT: Instance | undefined; 22 | 23 | function runWithInstance(instance: Instance | undefined, callback: () => void) { 24 | const parent = CURRENT; 25 | CURRENT = instance; 26 | try { 27 | return callback(); 28 | } finally { 29 | CURRENT = parent; 30 | } 31 | } 32 | 33 | function createInstance(): Instance { 34 | return { 35 | alive: true, 36 | mounted: false, 37 | count: 0, 38 | signals: [], 39 | syncs: [], 40 | effects: [], 41 | mounts: [], 42 | destroys: [], 43 | instances: [], 44 | }; 45 | } 46 | 47 | function flushSync(instance: Instance) { 48 | if (instance.alive) { 49 | runWithInstance(instance, () => { 50 | forEach(instance.syncs, (item) => item()); 51 | }); 52 | } 53 | } 54 | 55 | function flushEffects(instance: Instance) { 56 | if (instance.alive) { 57 | forEach(instance.effects, (item) => item()); 58 | } 59 | } 60 | 61 | function scheduleFlush(instance: Instance) { 62 | if (instance.alive) { 63 | if (instance.timeout) { 64 | clearTimeout(instance.timeout); 65 | } 66 | instance.timeout = setTimeout(() => { 67 | if (instance.alive) { 68 | flushEffects(instance); 69 | } 70 | }); 71 | } 72 | } 73 | 74 | function mount(instance: Instance) { 75 | if (instance.alive) { 76 | instance.mounted = true; 77 | 78 | forEach(instance.mounts, (item) => item()); 79 | scheduleFlush(instance); 80 | } 81 | } 82 | 83 | function destroy(instance: Instance) { 84 | if (instance.alive) { 85 | instance.alive = false; 86 | forEach(instance.instances, destroy); 87 | forEach(instance.destroys, (item) => item()); 88 | } 89 | } 90 | 91 | function create(setup: () => void) { 92 | const instance = createInstance(); 93 | runWithInstance(instance, () => { 94 | setup(); 95 | mount(instance); 96 | }); 97 | return instance; 98 | } 99 | 100 | function changed(signals: any[][], index: number, next: any[]) { 101 | const current = signals[index]; 102 | 103 | signals[index] = next; 104 | if (!current) { 105 | return true; 106 | } 107 | for (let i = 0, len = next.length; i < len; i += 1) { 108 | if (!Object.is(current[i], next[i])) { 109 | return true; 110 | } 111 | } 112 | return false; 113 | } 114 | 115 | /** 116 | * @private 117 | */ 118 | export function $$update(instance: Instance, value: T): T { 119 | if (instance.alive) { 120 | flushSync(instance); 121 | scheduleFlush(instance); 122 | } 123 | return value; 124 | } 125 | 126 | /** 127 | * @private 128 | */ 129 | export function $$sync( 130 | instance: Instance, 131 | next: () => any[], 132 | callback: () => void, 133 | ) { 134 | if (instance.alive) { 135 | const index = instance.count; 136 | instance.count += 1; 137 | const cb = () => { 138 | if (instance.alive && changed(instance.signals, index, next())) { 139 | const prev = instance.instances[index]; 140 | if (prev) { 141 | destroy(prev); 142 | } 143 | instance.instances[index] = create(callback); 144 | } 145 | }; 146 | cb(); 147 | instance.syncs.push(cb); 148 | } 149 | } 150 | 151 | /** 152 | * @private 153 | */ 154 | export function $$effect( 155 | instance: Instance, 156 | next: () => any[], 157 | callback: () => void, 158 | ) { 159 | if (instance.alive) { 160 | const index = instance.count; 161 | instance.count += 1; 162 | instance.effects.push(() => { 163 | if (instance.alive && changed(instance.signals, index, next())) { 164 | const prev = instance.instances[index]; 165 | if (prev) { 166 | destroy(prev); 167 | } 168 | instance.instances[index] = create(callback); 169 | } 170 | }); 171 | } 172 | } 173 | 174 | /** 175 | * @private 176 | */ 177 | export function $$context(): Instance { 178 | if (CURRENT) { 179 | return CURRENT; 180 | } 181 | throw new Error('Unexpected missing reactive boundary.'); 182 | } 183 | 184 | /** 185 | * @private 186 | */ 187 | export function $$mount(instance: Instance, callback: () => void): void { 188 | if (instance.alive) { 189 | if (instance.mounted) { 190 | callback(); 191 | } else { 192 | instance.mounts.push(callback); 193 | } 194 | } 195 | } 196 | 197 | /** 198 | * @private 199 | */ 200 | export function $$destroy(instance: Instance, callback: () => void): void { 201 | if (instance.alive) { 202 | instance.destroys.push(callback); 203 | } else { 204 | callback(); 205 | } 206 | } 207 | 208 | export function onMount(_callback: () => void): void { 209 | throw new Error('onMount is meant to be compile-time only.'); 210 | } 211 | 212 | export function onDestroy(_callback: () => void): void { 213 | throw new Error('onDestroy is meant to be compile-time only.'); 214 | } 215 | 216 | export function $$(setup: () => void): () => void { 217 | const instance = create(setup); 218 | return () => { 219 | destroy(instance); 220 | }; 221 | } 222 | 223 | export function $composable any)>(setup: T): T { 224 | return setup; 225 | } 226 | 227 | export function $(_value: T): void { 228 | throw new Error('$ is meant to be compile-time only.'); 229 | } 230 | 231 | export function $sync(_value: T): void { 232 | throw new Error('$sync is meant to be compile-time only.'); 233 | } 234 | 235 | export function $skip(_value: T): T { 236 | throw new Error('$skip is meant to be compile-time only.'); 237 | } 238 | 239 | export interface Store { 240 | get(): T; 241 | set?: (value: T) => void; 242 | subscribe(callback: () => void): () => void; 243 | } 244 | 245 | export function $store(_store: Store): T { 246 | throw new Error('$store is meant to be compile-time only.'); 247 | } 248 | 249 | /** 250 | * @private 251 | */ 252 | export function $$subscribe( 253 | instance: Instance, 254 | store: Store, 255 | listen: () => void, 256 | dependencies?: () => any[], 257 | update?: () => void, 258 | ) { 259 | if (instance.alive) { 260 | if (dependencies && update) { 261 | $$sync(instance, dependencies, update); 262 | } 263 | $$destroy(instance, store.subscribe(listen)); 264 | } else if (update) { 265 | update(); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /packages/silmaril/test/__snapshots__/compiler.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`$ > should compile to $$effect 1`] = ` 4 | "import { $$effect as _$$effect } from \\"silmaril\\"; 5 | import { $$context as _$$context } from \\"silmaril\\"; 6 | import { $$, $ } from 'silmaril'; 7 | $$(() => { 8 | let _ctx = _$$context(); 9 | _$$effect(_ctx, () => [], () => console.log('Example')); 10 | _$$effect(_ctx, () => [], () => console.log('Example')); 11 | });" 12 | `; 13 | 14 | exports[`$ > should not subscribe to unowned variables 1`] = ` 15 | "import { $$effect as _$$effect } from \\"silmaril\\"; 16 | import { $$context as _$$context } from \\"silmaril\\"; 17 | import { $$, $ } from 'silmaril'; 18 | let x = 0; 19 | $$(() => { 20 | let _ctx = _$$context(); 21 | _$$effect(_ctx, () => [], () => console.log(x)); 22 | _$$effect(_ctx, () => [], () => console.log(x)); 23 | });" 24 | `; 25 | 26 | exports[`$ > should subscribe to owned variables 1`] = ` 27 | "import { $$effect as _$$effect } from \\"silmaril\\"; 28 | import { $$context as _$$context } from \\"silmaril\\"; 29 | import { $$, $ } from 'silmaril'; 30 | $$(() => { 31 | let _ctx = _$$context(); 32 | let x = 0; 33 | _$$effect(_ctx, () => [x], () => console.log(x)); 34 | _$$effect(_ctx, () => [x], () => console.log(x)); 35 | });" 36 | `; 37 | 38 | exports[`$store > should allow tracking for stores 1`] = ` 39 | "import { $$update as _$$update } from \\"silmaril\\"; 40 | import { $$subscribe as _$$subscribe } from \\"silmaril\\"; 41 | import { $$context as _$$context } from \\"silmaril\\"; 42 | import { $$, $store } from 'silmaril'; 43 | $$(() => { 44 | let _ctx = _$$context(); 45 | let _store = someStore, 46 | example = _store.get(), 47 | /*$skip*/_subscribe = _$$subscribe(_ctx, _store, () => _$$update(_ctx, example = _store.get()), () => [example], () => _store.set(example)); 48 | $(console.log(example)); 49 | });" 50 | `; 51 | 52 | exports[`$store > should allow updates for mutable stores 1`] = ` 53 | "import { $$update as _$$update } from \\"silmaril\\"; 54 | import { $$subscribe as _$$subscribe } from \\"silmaril\\"; 55 | import { $$context as _$$context } from \\"silmaril\\"; 56 | import { $$, $store } from 'silmaril'; 57 | $$(() => { 58 | let _ctx = _$$context(); 59 | let _store = someStore, 60 | example = _store.get(), 61 | /*$skip*/_subscribe = _$$subscribe(_ctx, _store, () => _$$update(_ctx, example = _store.get()), () => [example], () => _store.set(example)); 62 | function mutate() { 63 | _$$update(_ctx, example = newValue); 64 | } 65 | });" 66 | `; 67 | 68 | exports[`$store > should compile $store with const 1`] = ` 69 | "import { $$update as _$$update } from \\"silmaril\\"; 70 | import { $$subscribe as _$$subscribe } from \\"silmaril\\"; 71 | import { $$context as _$$context } from \\"silmaril\\"; 72 | import { $$, $store } from 'silmaril'; 73 | $$(() => { 74 | let _ctx = _$$context(); 75 | let _store = someStore, 76 | example = _store.get(), 77 | /*$skip*/_subscribe = _$$subscribe(_ctx, _store, () => _$$update(_ctx, example = _store.get())); 78 | });" 79 | `; 80 | 81 | exports[`$store > should compile $store with let 1`] = ` 82 | "import { $$update as _$$update } from \\"silmaril\\"; 83 | import { $$subscribe as _$$subscribe } from \\"silmaril\\"; 84 | import { $$context as _$$context } from \\"silmaril\\"; 85 | import { $$, $store } from 'silmaril'; 86 | $$(() => { 87 | let _ctx = _$$context(); 88 | let _store = someStore, 89 | example = _store.get(), 90 | /*$skip*/_subscribe = _$$subscribe(_ctx, _store, () => _$$update(_ctx, example = _store.get()), () => [example], () => _store.set(example)); 91 | });" 92 | `; 93 | 94 | exports[`$sync > should compile to $$sync 1`] = ` 95 | "import { $$sync as _$$sync } from \\"silmaril\\"; 96 | import { $$context as _$$context } from \\"silmaril\\"; 97 | import { $$, $sync } from 'silmaril'; 98 | $$(() => { 99 | let _ctx = _$$context(); 100 | _$$sync(_ctx, () => [], () => console.log('Example')); 101 | _$$sync(_ctx, () => [], () => console.log('Example')); 102 | });" 103 | `; 104 | 105 | exports[`$sync > should not subscribe to unowned variables 1`] = ` 106 | "import { $$sync as _$$sync } from \\"silmaril\\"; 107 | import { $$context as _$$context } from \\"silmaril\\"; 108 | import { $$, $sync } from 'silmaril'; 109 | let x = 0; 110 | $$(() => { 111 | let _ctx = _$$context(); 112 | _$$sync(_ctx, () => [], () => console.log(x)); 113 | _$$sync(_ctx, () => [], () => console.log(x)); 114 | });" 115 | `; 116 | 117 | exports[`$sync > should subscribe to owned variables 1`] = ` 118 | "import { $$sync as _$$sync } from \\"silmaril\\"; 119 | import { $$context as _$$context } from \\"silmaril\\"; 120 | import { $$, $sync } from 'silmaril'; 121 | $$(() => { 122 | let _ctx = _$$context(); 123 | let x = 0; 124 | _$$sync(_ctx, () => [x], () => console.log(x)); 125 | _$$sync(_ctx, () => [x], () => console.log(x)); 126 | });" 127 | `; 128 | 129 | exports[`let > should compile on AssignmentExpression 1`] = ` 130 | "import { $$update as _$$update } from \\"silmaril\\"; 131 | import { $$context as _$$context } from \\"silmaril\\"; 132 | import { $$ } from 'silmaril'; 133 | $$(() => { 134 | let _ctx = _$$context(); 135 | let x = 0; 136 | function increment() { 137 | _$$update(_ctx, x += 1); 138 | } 139 | });" 140 | `; 141 | 142 | exports[`let > should compile on UpdateExpression 1`] = ` 143 | "import { $$update as _$$update } from \\"silmaril\\"; 144 | import { $$context as _$$context } from \\"silmaril\\"; 145 | import { $$ } from 'silmaril'; 146 | $$(() => { 147 | let _ctx = _$$context(); 148 | let x = 0; 149 | function increment() { 150 | _$$update(_ctx, x++); 151 | } 152 | });" 153 | `; 154 | 155 | exports[`let > should compile when accessing owned variables 1`] = ` 156 | "import { $$update as _$$update } from \\"silmaril\\"; 157 | import { $$sync as _$$sync } from \\"silmaril\\"; 158 | import { $$context as _$$context } from \\"silmaril\\"; 159 | import { $$ } from 'silmaril'; 160 | $$(() => { 161 | let _ctx = _$$context(); 162 | let greeting = 'Hello'; 163 | let receiver = 'World'; 164 | let message, 165 | /*$skip*/_computed = _$$sync(_ctx, () => [greeting, receiver], () => _$$update(_ctx, message = greeting + ' ' + receiver)); 166 | });" 167 | `; 168 | 169 | exports[`let > should not compile when not accessing owned variables 1`] = ` 170 | "import { $$context as _$$context } from \\"silmaril\\"; 171 | import { $$ } from 'silmaril'; 172 | $$(() => { 173 | let _ctx = _$$context(); 174 | let message = Math.random(); 175 | });" 176 | `; 177 | 178 | exports[`onDestroy > should compile to $$destroy 1`] = ` 179 | "import { $$destroy as _$$destroy } from \\"silmaril\\"; 180 | import { $$context as _$$context } from \\"silmaril\\"; 181 | import { $$, onDestroy } from 'silmaril'; 182 | $$(() => { 183 | let _ctx = _$$context(); 184 | _$$destroy(_ctx, () => console.log('Example')); 185 | });" 186 | `; 187 | 188 | exports[`onMount > should compile to $$mount 1`] = ` 189 | "import { $$mount as _$$mount } from \\"silmaril\\"; 190 | import { $$context as _$$context } from \\"silmaril\\"; 191 | import { $$, onMount } from 'silmaril'; 192 | $$(() => { 193 | let _ctx = _$$context(); 194 | _$$mount(_ctx, () => sconsole.log('Example')); 195 | });" 196 | `; 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # silmaril 2 | 3 | > Compile-time reactivity for JS 4 | 5 | [![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&logo=codesandbox)](https://codesandbox.io/s/github/LXSMNSYC/silmaril/tree/main/examples/demo) 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm install --save silmaril 11 | ``` 12 | 13 | ```bash 14 | yarn add silmaril 15 | ``` 16 | 17 | ```bash 18 | pnpm add silmaril 19 | ``` 20 | 21 | ## Features 22 | 23 | - Compile-time reactivity 24 | - Minimal reactive runtime 25 | - Auto-memoization 26 | - Stores 27 | 28 | ## Requirement 29 | 30 | Due 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`. 31 | 32 | ## Usage 33 | 34 | ### Basic reactivity 35 | 36 | `$$` 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. 37 | 38 | Variable changes and reactions are only limited in `$$` (even for nested `$$` calls). 39 | 40 | ```js 41 | import { $$, $ } from 'silmaril'; 42 | 43 | $$(() => { 44 | // Create a "reactive" variable 45 | let count = 0; 46 | 47 | // Log count for changes 48 | $(console.log('Count: ', count)); 49 | 50 | 51 | function multiply() { 52 | // Update count 53 | count *= 100; 54 | } 55 | 56 | multiply(); 57 | // After some time, this code logs `Count: 100`. 58 | }); 59 | ``` 60 | 61 | `$` will know which variables to track with, but it can only know if the variable is accessed in that same call. 62 | 63 | ```js 64 | import { $$, $ } from 'silmaril'; 65 | 66 | $$(() => { 67 | // Create a "reactive" variable 68 | let count = 0; 69 | let prefix = 'Count'; 70 | 71 | function log(current) { 72 | // `prefix` is not tracked 73 | console.log(`${prefix}: `, current); 74 | } 75 | 76 | // This only tracks `count` 77 | $(log(count)); 78 | }); 79 | ``` 80 | 81 | `$` can also accept a function expression, and has the same tracking capabilities. 82 | 83 | ```js 84 | $(() => { 85 | // This tracks `count` 86 | console.log('Count:', count); 87 | }); 88 | ``` 89 | 90 | `$` will only run if the tracked variables have actually changed (except for the first run), which means that it has some "auto-memoization". 91 | 92 | ### Computed variables 93 | 94 | If a reactive variable references another, the variable becomes computed, which means that it will re-evaluate everytime the referenced variables changes. 95 | 96 | ```js 97 | import { $$, $ } from 'silmaril'; 98 | 99 | $$(() => { 100 | // Create a "reactive" variable 101 | let count = 0; 102 | 103 | // Create a "reactive" const variable. 104 | const message = `Count: ${count}`; 105 | 106 | // This only tracks `message` 107 | $(console.log(message)); 108 | 109 | count = 100; // Logs 'Count: 100' 110 | }); 111 | ``` 112 | 113 | Updates on computed variables are synchronous. 114 | 115 | ```js 116 | import { $$ } from 'silmaril'; 117 | 118 | $$(() => { 119 | let count = 0; 120 | const message = `Count: ${count}`; 121 | count = 100; // message = Count: 100 122 | count = 200; // message = Count: 200 123 | }); 124 | ``` 125 | 126 | Computed variables are also writable if declared with `let`. 127 | 128 | ```js 129 | import { $$, $sync } from 'silmaril'; 130 | 131 | $$(() => { 132 | let count = 0; 133 | let message = `Count: ${count}`; 134 | $sync(console.log('Log', message)); // Log Count: 0 135 | count = 100; // Log Count: 100 136 | message = 'Hello World'; // Log Hello World 137 | count = 200; // Log Count: 200 138 | }); 139 | ``` 140 | 141 | ### Lifecycles 142 | 143 | #### `onMount` 144 | 145 | `onMount` can be used to detect once `$$` has finished the setup. 146 | 147 | ```js 148 | import { $$, onMount } from 'silmaril'; 149 | 150 | $$(() => { 151 | onMount(() => { 152 | console.log('Mounted!'); 153 | }); 154 | console.log('Not mounted yet!'); 155 | }); 156 | ``` 157 | 158 | `onMount` can also be used in `$`, `$sync`, `$composable` and computed variables. 159 | 160 | #### `onDestroy` 161 | 162 | `$$` returns a callback that allows disposing the reactive boundary. You can use `onDestroy` to detect when this happens. 163 | 164 | ```js 165 | import { $$, onDestroy } from 'silmaril'; 166 | 167 | const stop = $$(() => { 168 | onDestroy(() => { 169 | console.log('Destroyed!'); 170 | }); 171 | }); 172 | 173 | // ... 174 | stop(); 175 | ``` 176 | 177 | `onDestroy` can also be used in `$`, `$sync`, `$composable` and computed variables. 178 | 179 | ### Synchronous tracking 180 | 181 | `$` 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). 182 | 183 | `$sync` provides synchronous tracking. 184 | 185 | ```js 186 | import { $$, $, $sync } from 'silmaril'; 187 | 188 | $$(() => { 189 | // Create a "reactive" variable 190 | let count = 0; 191 | 192 | // Create a "reactive" const variable. 193 | const message = `Count: ${count}`; 194 | 195 | $sync(console.log('Sync', message)); // Logs "Sync Count: 0" 196 | $(console.log('Async', message)); 197 | 198 | count = 100; // Logs "Sync Count: 100" 199 | count = 200; // Logs "Sync Count: 200" 200 | 201 | // After some time the code ends, logs "Async Count: 200" 202 | }); 203 | ``` 204 | 205 | ### Stores 206 | 207 | Reactivity is isolated in `$$`, but there are multiple ways to expose it outside `$$` e.g. emulating event emitters, using observables, global state management, etc. 208 | 209 | `silmaril/store` provides a simple API for this, and `$store` allows two-way (or one-way) binding for stores. 210 | 211 | ```js 212 | import { $$, $, $sync, $store } from 'silmaril'; 213 | import Store from 'silmaril/store'; 214 | 215 | // Create a store 216 | const count = new Store(100); 217 | 218 | // Subscribe to it 219 | count.subscribe((current) => { 220 | console.log('Raw Count:', current); 221 | }); 222 | 223 | $$(() => { 224 | // Bind the store to a reactive variable 225 | let current = $store(count); 226 | // `const` can also be used as an alternative 227 | // for enforcing one-way binding 228 | 229 | // Tracking the bound variable 230 | $sync(console.log('Sync Count:', current)); 231 | $(console.log('Async Count:', current)); 232 | 233 | // Mutate the variable (also mutates the store) 234 | current += 100; 235 | 236 | // Logs 237 | // Sync Count: 100 238 | // Raw Count: 200 239 | // Sync Count: 200 240 | // Async Count: 200 241 | }); 242 | ``` 243 | 244 | `$store` can accept any kind of implementation as long as it follows the following interface: 245 | 246 | - `subscribe(callback: Function): Function`: accepts a callback and returns a cleanup callback 247 | - `get()`: returns the current state of the store 248 | - `set(state)`: optional, mutates the state of the store. 249 | 250 | ### Composition 251 | 252 | #### `$composable` 253 | 254 | `$composable` allows composing functions that can be used in `$$`, `$sync`, `$`, another `$composable` or computed variables. 255 | 256 | ```js 257 | import { $$, $sync, $composable, $store, onDestroy } from 'silmaril'; 258 | import Store from 'silmaril/store'; 259 | 260 | // Create a composable 261 | const useSquared = $composable((store) => { 262 | // Bind the input store to a variable 263 | const input = $store(store); 264 | 265 | // Create a store 266 | const squaredStore = new Store(0); 267 | 268 | // Make sure to cleanup the store 269 | onDestroy(() => squaredStore.destroy()); 270 | 271 | // Update the store based on the bound input store 272 | $sync(squaredStore.set(input ** 2)); 273 | 274 | // Return the store 275 | return squaredStore; 276 | }); 277 | 278 | $$(() => { 279 | // Create a store 280 | const store = new Store(0); 281 | 282 | // Bind it 283 | let input = $store(store); 284 | 285 | // Track the value of the store 286 | $sync(console.log('Value', input)); 287 | 288 | // Create a "squared" store based on the input store 289 | // then bind it 290 | const squared = $store(useSquared(store)); 291 | 292 | // Track the squared store 293 | $sync(console.log('Squared', squared)); 294 | 295 | // Update the input store 296 | input = 100; 297 | 298 | // Logs 299 | // Count: 0 300 | // Count: 100 301 | // Count: 200 302 | }); 303 | ``` 304 | 305 | #### `$` and `$sync` 306 | 307 | Both `$` and `$sync` behave much like `$$`: variables become reactive, `onMount` and `onDestroy` can be used, same goes to other APIs. 308 | 309 | ```js 310 | import { $$, $, onDestroy } from 'silmaril'; 311 | 312 | $$(() => { 313 | let y = 0; 314 | $(() => { 315 | let x = 0; 316 | 317 | $(console.log(x + y)); 318 | 319 | onDestroy(() => { 320 | console.log('This will be cleaned up when `y` changes'); 321 | }); 322 | 323 | x += 100; 324 | }); 325 | y += 100; 326 | }); 327 | ``` 328 | 329 | ## Inspirations/Prior Art 330 | 331 | - [Svelte](https://svelte.dev/) 332 | - [Malina](https://malinajs.github.io/docs/) 333 | - [`solid-labels`](https://github.com/LXSMNSYC/solid-labels) 334 | - [Vue's Reactivity Transform](https://github.com/vuejs/rfcs/discussions/369) 335 | 336 | ## Sponsors 337 | 338 | ![Sponsors](https://github.com/lxsmnsyc/sponsors/blob/main/sponsors.svg?raw=true) 339 | 340 | ## License 341 | 342 | MIT © [lxsmnsyc](https://github.com/lxsmnsyc) 343 | -------------------------------------------------------------------------------- /packages/silmaril/README.md: -------------------------------------------------------------------------------- 1 | # silmaril 2 | 3 | > Compile-time reactivity for JS 4 | 5 | [![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&logo=codesandbox)](https://codesandbox.io/s/github/LXSMNSYC/silmaril/tree/main/examples/demo) 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm install --save silmaril 11 | ``` 12 | 13 | ```bash 14 | yarn add silmaril 15 | ``` 16 | 17 | ```bash 18 | pnpm add silmaril 19 | ``` 20 | 21 | ## Features 22 | 23 | - Compile-time reactivity 24 | - Minimal reactive runtime 25 | - Auto-memoization 26 | - Stores 27 | 28 | ## Requirement 29 | 30 | Due 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`. 31 | 32 | ## Usage 33 | 34 | ### Basic reactivity 35 | 36 | `$$` 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. 37 | 38 | Variable changes and reactions are only limited in `$$` (even for nested `$$` calls). 39 | 40 | ```js 41 | import { $$, $ } from 'silmaril'; 42 | 43 | $$(() => { 44 | // Create a "reactive" variable 45 | let count = 0; 46 | 47 | // Log count for changes 48 | $(console.log('Count: ', count)); 49 | 50 | 51 | function multiply() { 52 | // Update count 53 | count *= 100; 54 | } 55 | 56 | multiply(); 57 | // After some time, this code logs `Count: 100`. 58 | }); 59 | ``` 60 | 61 | `$` will know which variables to track with, but it can only know if the variable is accessed in that same call. 62 | 63 | ```js 64 | import { $$, $ } from 'silmaril'; 65 | 66 | $$(() => { 67 | // Create a "reactive" variable 68 | let count = 0; 69 | let prefix = 'Count'; 70 | 71 | function log(current) { 72 | // `prefix` is not tracked 73 | console.log(`${prefix}: `, current); 74 | } 75 | 76 | // This only tracks `count` 77 | $(log(count)); 78 | }); 79 | ``` 80 | 81 | `$` can also accept a function expression, and has the same tracking capabilities. 82 | 83 | ```js 84 | $(() => { 85 | // This tracks `count` 86 | console.log('Count:', count); 87 | }); 88 | ``` 89 | 90 | `$` will only run if the tracked variables have actually changed (except for the first run), which means that it has some "auto-memoization". 91 | 92 | ### Computed variables 93 | 94 | If a reactive variable references another, the variable becomes computed, which means that it will re-evaluate everytime the referenced variables changes. 95 | 96 | ```js 97 | import { $$, $ } from 'silmaril'; 98 | 99 | $$(() => { 100 | // Create a "reactive" variable 101 | let count = 0; 102 | 103 | // Create a "reactive" const variable. 104 | const message = `Count: ${count}`; 105 | 106 | // This only tracks `message` 107 | $(console.log(message)); 108 | 109 | count = 100; // Logs 'Count: 100' 110 | }); 111 | ``` 112 | 113 | Updates on computed variables are synchronous. 114 | 115 | ```js 116 | import { $$ } from 'silmaril'; 117 | 118 | $$(() => { 119 | let count = 0; 120 | const message = `Count: ${count}`; 121 | count = 100; // message = Count: 100 122 | count = 200; // message = Count: 200 123 | }); 124 | ``` 125 | 126 | Computed variables are also writable if declared with `let`. 127 | 128 | ```js 129 | import { $$, $sync } from 'silmaril'; 130 | 131 | $$(() => { 132 | let count = 0; 133 | let message = `Count: ${count}`; 134 | $sync(console.log('Log', message)); // Log Count: 0 135 | count = 100; // Log Count: 100 136 | message = 'Hello World'; // Log Hello World 137 | count = 200; // Log Count: 200 138 | }); 139 | ``` 140 | 141 | ### Lifecycles 142 | 143 | #### `onMount` 144 | 145 | `onMount` can be used to detect once `$$` has finished the setup. 146 | 147 | ```js 148 | import { $$, onMount } from 'silmaril'; 149 | 150 | $$(() => { 151 | onMount(() => { 152 | console.log('Mounted!'); 153 | }); 154 | console.log('Not mounted yet!'); 155 | }); 156 | ``` 157 | 158 | `onMount` can also be used in `$`, `$sync`, `$composable` and computed variables. 159 | 160 | #### `onDestroy` 161 | 162 | `$$` returns a callback that allows disposing the reactive boundary. You can use `onDestroy` to detect when this happens. 163 | 164 | ```js 165 | import { $$, onDestroy } from 'silmaril'; 166 | 167 | const stop = $$(() => { 168 | onDestroy(() => { 169 | console.log('Destroyed!'); 170 | }); 171 | }); 172 | 173 | // ... 174 | stop(); 175 | ``` 176 | 177 | `onDestroy` can also be used in `$`, `$sync`, `$composable` and computed variables. 178 | 179 | ### Synchronous tracking 180 | 181 | `$` 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). 182 | 183 | `$sync` provides synchronous tracking. 184 | 185 | ```js 186 | import { $$, $, $sync } from 'silmaril'; 187 | 188 | $$(() => { 189 | // Create a "reactive" variable 190 | let count = 0; 191 | 192 | // Create a "reactive" const variable. 193 | const message = `Count: ${count}`; 194 | 195 | $sync(console.log('Sync', message)); // Logs "Sync Count: 0" 196 | $(console.log('Async', message)); 197 | 198 | count = 100; // Logs "Sync Count: 100" 199 | count = 200; // Logs "Sync Count: 200" 200 | 201 | // After some time the code ends, logs "Async Count: 200" 202 | }); 203 | ``` 204 | 205 | ### Stores 206 | 207 | Reactivity is isolated in `$$`, but there are multiple ways to expose it outside `$$` e.g. emulating event emitters, using observables, global state management, etc. 208 | 209 | `silmaril/store` provides a simple API for this, and `$store` allows two-way (or one-way) binding for stores. 210 | 211 | ```js 212 | import { $$, $, $sync, $store } from 'silmaril'; 213 | import Store from 'silmaril/store'; 214 | 215 | // Create a store 216 | const count = new Store(100); 217 | 218 | // Subscribe to it 219 | count.subscribe((current) => { 220 | console.log('Raw Count:', current); 221 | }); 222 | 223 | $$(() => { 224 | // Bind the store to a reactive variable 225 | let current = $store(count); 226 | // `const` can also be used as an alternative 227 | // for enforcing one-way binding 228 | 229 | // Tracking the bound variable 230 | $sync(console.log('Sync Count:', current)); 231 | $(console.log('Async Count:', current)); 232 | 233 | // Mutate the variable (also mutates the store) 234 | current += 100; 235 | 236 | // Logs 237 | // Sync Count: 100 238 | // Raw Count: 200 239 | // Sync Count: 200 240 | // Async Count: 200 241 | }); 242 | ``` 243 | 244 | `$store` can accept any kind of implementation as long as it follows the following interface: 245 | 246 | - `subscribe(callback: Function): Function`: accepts a callback and returns a cleanup callback 247 | - `get()`: returns the current state of the store 248 | - `set(state)`: optional, mutates the state of the store. 249 | 250 | ### Composition 251 | 252 | #### `$composable` 253 | 254 | `$composable` allows composing functions that can be used in `$$`, `$sync`, `$`, another `$composable` or computed variables. 255 | 256 | ```js 257 | import { $$, $sync, $composable, $store, onDestroy } from 'silmaril'; 258 | import Store from 'silmaril/store'; 259 | 260 | // Create a composable 261 | const useSquared = $composable((store) => { 262 | // Bind the input store to a variable 263 | const input = $store(store); 264 | 265 | // Create a store 266 | const squaredStore = new Store(0); 267 | 268 | // Make sure to cleanup the store 269 | onDestroy(() => squaredStore.destroy()); 270 | 271 | // Update the store based on the bound input store 272 | $sync(squaredStore.set(input ** 2)); 273 | 274 | // Return the store 275 | return squaredStore; 276 | }); 277 | 278 | $$(() => { 279 | // Create a store 280 | const store = new Store(0); 281 | 282 | // Bind it 283 | let input = $store(store); 284 | 285 | // Track the value of the store 286 | $sync(console.log('Value', input)); 287 | 288 | // Create a "squared" store based on the input store 289 | // then bind it 290 | const squared = $store(useSquared(store)); 291 | 292 | // Track the squared store 293 | $sync(console.log('Squared', squared)); 294 | 295 | // Update the input store 296 | input = 100; 297 | 298 | // Logs 299 | // Count: 0 300 | // Count: 100 301 | // Count: 200 302 | }); 303 | ``` 304 | 305 | #### `$` and `$sync` 306 | 307 | Both `$` and `$sync` behave much like `$$`: variables become reactive, `onMount` and `onDestroy` can be used, same goes to other APIs. 308 | 309 | ```js 310 | import { $$, $, onDestroy } from 'silmaril'; 311 | 312 | $$(() => { 313 | let y = 0; 314 | $(() => { 315 | let x = 0; 316 | 317 | $(console.log(x + y)); 318 | 319 | onDestroy(() => { 320 | console.log('This will be cleaned up when `y` changes'); 321 | }); 322 | 323 | x += 100; 324 | }); 325 | y += 100; 326 | }); 327 | ``` 328 | 329 | ## Inspirations/Prior Art 330 | 331 | - [Svelte](https://svelte.dev/) 332 | - [Malina](https://malinajs.github.io/docs/) 333 | - [`solid-labels`](https://github.com/LXSMNSYC/solid-labels) 334 | - [Vue's Reactivity Transform](https://github.com/vuejs/rfcs/discussions/369) 335 | 336 | ## Sponsors 337 | 338 | ![Sponsors](https://github.com/lxsmnsyc/sponsors/blob/main/sponsors.svg?raw=true) 339 | 340 | ## License 341 | 342 | MIT © [lxsmnsyc](https://github.com/lxsmnsyc) 343 | -------------------------------------------------------------------------------- /packages/silmaril/babel/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginObj, PluginPass } from '@babel/core'; 2 | import { addNamed } from '@babel/helper-module-imports'; 3 | import { NodePath } from '@babel/traverse'; 4 | import * as t from '@babel/types'; 5 | import { getImportSpecifierName } from './checks'; 6 | import unwrapNode from './unwrap-node'; 7 | 8 | const SOURCE_MODULE = 'silmaril'; 9 | 10 | type SilmarilTopLevel = '$$' | '$composable'; 11 | type SilmarilEffects = '$' | '$sync'; 12 | type SilmarilLifecycles = 'onMount' | 'onDestroy'; 13 | type SilmarilStores = '$store'; 14 | 15 | type SilmarilCTFS = 16 | | SilmarilTopLevel 17 | | SilmarilEffects 18 | | SilmarilLifecycles 19 | | SilmarilStores; 20 | 21 | const TRACKED_IMPORTS: Record = { 22 | $$: true, 23 | $: true, 24 | $sync: true, 25 | $store: true, 26 | $composable: true, 27 | onMount: true, 28 | onDestroy: true, 29 | }; 30 | 31 | const TRUE_CONTEXT = '$$context'; 32 | const TRUE_UPDATE = '$$update'; 33 | const TRUE_EFFECT = '$$effect'; 34 | const TRUE_SYNC = '$$sync'; 35 | const TRUE_SUBSCRIBE = '$$subscribe'; 36 | const TRUE_ON_MOUNT = '$$mount'; 37 | const TRUE_ON_DESTROY = '$$destroy'; 38 | 39 | const SKIP = '$skip'; 40 | const CAN_SKIP = /^\s*\$skip\s*$/; 41 | 42 | function canSkip(node: t.Node) { 43 | if (node.leadingComments) { 44 | for (let i = 0, len = node.leadingComments.length; i < len; i += 1) { 45 | if (CAN_SKIP.test(node.leadingComments[i].value)) { 46 | return true; 47 | } 48 | } 49 | } 50 | return false; 51 | } 52 | 53 | type ImportIdentifiers = { 54 | [key in SilmarilCTFS]: Set; 55 | } 56 | 57 | interface StateContext { 58 | hooks: Map; 59 | identifiers: ImportIdentifiers; 60 | } 61 | 62 | function getHookIdentifier( 63 | ctx: StateContext, 64 | path: NodePath, 65 | name: string, 66 | ): t.Identifier { 67 | const current = ctx.hooks.get(name); 68 | if (current) { 69 | return current; 70 | } 71 | const newID = addNamed(path, name, SOURCE_MODULE); 72 | ctx.hooks.set(name, newID); 73 | return newID; 74 | } 75 | 76 | function extractImportIdentifiers( 77 | ctx: StateContext, 78 | path: NodePath, 79 | ) { 80 | if (path.node.source.value === SOURCE_MODULE) { 81 | for (let i = 0, len = path.node.specifiers.length; i < len; i += 1) { 82 | const specifier = path.node.specifiers[i]; 83 | if (t.isImportSpecifier(specifier)) { 84 | const specifierName = getImportSpecifierName(specifier); 85 | if (specifierName in TRACKED_IMPORTS) { 86 | ctx.identifiers[specifierName as SilmarilCTFS].add(specifier.local); 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | function unwrapLVal(identifiers: Set, value: t.LVal) { 94 | if (canSkip(value)) { 95 | return; 96 | } 97 | if (t.isIdentifier(value)) { 98 | identifiers.add(value); 99 | } else if (t.isRestElement(value)) { 100 | unwrapLVal(identifiers, value.argument); 101 | } else if (t.isAssignmentPattern(value)) { 102 | unwrapLVal(identifiers, value.left); 103 | } else if (t.isArrayPattern(value)) { 104 | for (let i = 0, len = value.elements.length; i < len; i += 1) { 105 | const el = value.elements[i]; 106 | if (el) { 107 | unwrapLVal(identifiers, el); 108 | } 109 | } 110 | } else if (t.isObjectPattern(value)) { 111 | for (let i = 0, len = value.properties.length; i < len; i += 1) { 112 | const el = value.properties[i]; 113 | if (t.isRestElement(el)) { 114 | unwrapLVal(identifiers, el.argument); 115 | } else if (t.isLVal(el.value)) { 116 | unwrapLVal(identifiers, el.value); 117 | } 118 | } 119 | } 120 | } 121 | 122 | function getDependencies( 123 | path: NodePath, 124 | identifiers: Set, 125 | ): Set { 126 | // Collect dependencies 127 | const dependencies = new Set(); 128 | path.traverse({ 129 | Expression(p) { 130 | if (t.isIdentifier(p.node) && !canSkip(p.node)) { 131 | const binding = p.scope.getBindingIdentifier(p.node.name); 132 | if (binding && identifiers.has(binding)) { 133 | dependencies.add(binding); 134 | } 135 | } 136 | }, 137 | }); 138 | return dependencies; 139 | } 140 | 141 | function transformTracking( 142 | ctx: StateContext, 143 | path: NodePath, 144 | instanceID: t.Identifier, 145 | dependencies: Set, 146 | type: string, 147 | ) { 148 | const arg = path.node.arguments[0]; 149 | 150 | if (!t.isExpression(arg)) { 151 | throw new Error(`${type} can only accept Expression.`); 152 | } 153 | 154 | let result: t.Expression; 155 | 156 | if ( 157 | unwrapNode(arg, t.isArrowFunctionExpression) 158 | || unwrapNode(arg, t.isFunctionExpression) 159 | ) { 160 | result = arg; 161 | } else { 162 | result = t.arrowFunctionExpression( 163 | [], 164 | arg, 165 | ); 166 | } 167 | 168 | path.replaceWith( 169 | t.callExpression( 170 | getHookIdentifier(ctx, path, type), 171 | [ 172 | instanceID, 173 | t.arrowFunctionExpression([], t.arrayExpression(Array.from(dependencies))), 174 | result, 175 | ], 176 | ), 177 | ); 178 | } 179 | 180 | function transformComputed( 181 | ctx: StateContext, 182 | path: NodePath, 183 | identifiers: Set, 184 | instanceID: t.Identifier, 185 | id: t.LVal, 186 | init: t.Expression, 187 | ) { 188 | const dependencies = getDependencies(path, identifiers); 189 | if (dependencies.size) { 190 | path.insertAfter( 191 | t.addComment( 192 | t.variableDeclarator( 193 | path.scope.generateUidIdentifier('computed'), 194 | t.callExpression( 195 | getHookIdentifier(ctx, path, TRUE_SYNC), 196 | [ 197 | instanceID, 198 | t.arrowFunctionExpression( 199 | [], 200 | t.arrayExpression(Array.from(dependencies)), 201 | ), 202 | t.arrowFunctionExpression( 203 | [], 204 | t.assignmentExpression('=', id, init), 205 | ), 206 | ], 207 | ), 208 | ), 209 | 'leading', 210 | SKIP, 211 | ), 212 | ); 213 | path.node.init = undefined; 214 | } 215 | } 216 | 217 | function transformStore( 218 | ctx: StateContext, 219 | path: NodePath, 220 | instanceID: t.Identifier, 221 | id: t.LVal, 222 | init: t.CallExpression, 223 | kind: t.VariableDeclaration['kind'], 224 | ) { 225 | if (init.arguments.length !== 1) { 226 | throw new Error('$store can only accept a single argument.'); 227 | } 228 | const storeArg = init.arguments[0]; 229 | if (!t.isExpression(storeArg)) { 230 | throw new Error('$store can only accept expressions.'); 231 | } 232 | if (!t.isIdentifier(id)) { 233 | throw new Error('$store is only limited to identifiers.'); 234 | } 235 | const storeIdentifier = path.scope.generateUidIdentifier('store'); 236 | path.insertBefore( 237 | t.variableDeclarator(storeIdentifier, storeArg), 238 | ); 239 | const read = t.callExpression( 240 | t.memberExpression( 241 | storeIdentifier, 242 | t.identifier('get'), 243 | ), 244 | [], 245 | ); 246 | const args: t.Expression[] = kind === 'const' ? [] : [ 247 | t.arrowFunctionExpression( 248 | [], 249 | t.arrayExpression([id]), 250 | ), 251 | t.arrowFunctionExpression( 252 | [], 253 | t.callExpression( 254 | t.memberExpression( 255 | storeIdentifier, 256 | t.identifier('set'), 257 | ), 258 | [id], 259 | ), 260 | ), 261 | ]; 262 | path.insertAfter( 263 | t.addComment( 264 | t.variableDeclarator( 265 | path.scope.generateUidIdentifier('subscribe'), 266 | t.callExpression( 267 | getHookIdentifier(ctx, path, TRUE_SUBSCRIBE), 268 | [ 269 | instanceID, 270 | storeIdentifier, 271 | t.arrowFunctionExpression( 272 | [], 273 | t.assignmentExpression( 274 | '=', 275 | id, 276 | read, 277 | ), 278 | ), 279 | ...args, 280 | ], 281 | ), 282 | ), 283 | 'leading', 284 | SKIP, 285 | ), 286 | ); 287 | path.node.init = read; 288 | } 289 | 290 | function traverseIdentifiers( 291 | ctx: StateContext, 292 | path: NodePath, 293 | instanceID: t.Identifier, 294 | arg: t.Function, 295 | ) { 296 | const identifiers = new Set(); 297 | path.traverse({ 298 | VariableDeclaration(p) { 299 | const functionParent = p.getFunctionParent(); 300 | if (functionParent && functionParent.node === arg) { 301 | const { kind } = p.node; 302 | p.traverse({ 303 | VariableDeclarator(child) { 304 | if (child.parentPath === p) { 305 | if (canSkip(child.node)) { 306 | return; 307 | } 308 | unwrapLVal(identifiers, child.node.id); 309 | if (child.node.init) { 310 | if (t.isCallExpression(child.node.init) && t.isIdentifier(child.node.init.callee)) { 311 | const binding = child.scope.getBindingIdentifier(child.node.init.callee.name); 312 | if (binding && ctx.identifiers.$store.has(binding)) { 313 | transformStore( 314 | ctx, 315 | child, 316 | instanceID, 317 | child.node.id, 318 | child.node.init, 319 | kind, 320 | ); 321 | return; 322 | } 323 | } 324 | transformComputed( 325 | ctx, 326 | child, 327 | identifiers, 328 | instanceID, 329 | child.node.id, 330 | child.node.init, 331 | ); 332 | } 333 | } 334 | }, 335 | }); 336 | if (p.node.kind === 'const') { 337 | p.node.kind = 'let'; 338 | } 339 | } 340 | }, 341 | }); 342 | return identifiers; 343 | } 344 | 345 | function checkValidAssignment( 346 | path: NodePath, 347 | identifiers: Set, 348 | marked: Set, 349 | ) { 350 | for (const item of marked) { 351 | const binding = path.scope.getBindingIdentifier(item.name); 352 | if (binding && identifiers.has(binding)) { 353 | return true; 354 | } 355 | } 356 | return false; 357 | } 358 | 359 | function transformReads( 360 | ctx: StateContext, 361 | path: NodePath, 362 | arg: t.Function, 363 | identifiers: Set, 364 | instanceID: t.Identifier, 365 | ) { 366 | path.traverse({ 367 | // Step 2: Change all UpdateExpression and AssignmentExpression 368 | UpdateExpression(p) { 369 | const { argument } = p.node; 370 | if (t.isIdentifier(argument)) { 371 | const binding = p.scope.getBindingIdentifier(argument.name); 372 | if (binding && identifiers.has(binding)) { 373 | p.replaceWith( 374 | t.callExpression( 375 | getHookIdentifier(ctx, p, TRUE_UPDATE), 376 | [ 377 | instanceID, 378 | p.node, 379 | ], 380 | ), 381 | ); 382 | p.skip(); 383 | } 384 | } 385 | }, 386 | AssignmentExpression(p) { 387 | const marked = new Set(); 388 | unwrapLVal(marked, p.node.left); 389 | if (checkValidAssignment(p, identifiers, marked)) { 390 | p.replaceWith( 391 | t.callExpression( 392 | getHookIdentifier(ctx, p, TRUE_UPDATE), 393 | [ 394 | instanceID, 395 | p.node, 396 | ], 397 | ), 398 | ); 399 | p.skip(); 400 | } 401 | }, 402 | CallExpression(p) { 403 | // If the assignment occurs in the same function, ignore 404 | const functionParent = p.getFunctionParent(); 405 | if (functionParent && functionParent.node === arg) { 406 | const { callee } = p.node; 407 | 408 | const trueIdentifier = unwrapNode(callee, t.isIdentifier); 409 | if (trueIdentifier) { 410 | const binding = p.scope.getBindingIdentifier(trueIdentifier.name); 411 | if (binding) { 412 | if (ctx.identifiers.$.has(binding)) { 413 | const dependencies = getDependencies(p, identifiers); 414 | transformTracking( 415 | ctx, 416 | p, 417 | instanceID, 418 | dependencies, 419 | TRUE_EFFECT, 420 | ); 421 | } 422 | if (ctx.identifiers.$sync.has(binding)) { 423 | const dependencies = getDependencies(p, identifiers); 424 | transformTracking( 425 | ctx, 426 | p, 427 | instanceID, 428 | dependencies, 429 | TRUE_SYNC, 430 | ); 431 | } 432 | if (ctx.identifiers.onMount.has(binding)) { 433 | p.node.callee = getHookIdentifier(ctx, p, TRUE_ON_MOUNT); 434 | p.node.arguments = [ 435 | instanceID, 436 | ...p.node.arguments, 437 | ]; 438 | } 439 | if (ctx.identifiers.onDestroy.has(binding)) { 440 | p.node.callee = getHookIdentifier(ctx, p, TRUE_ON_DESTROY); 441 | p.node.arguments = [ 442 | instanceID, 443 | ...p.node.arguments, 444 | ]; 445 | } 446 | } 447 | } 448 | } 449 | }, 450 | }); 451 | } 452 | 453 | function transformSetup( 454 | ctx: StateContext, 455 | path: NodePath, 456 | type: SilmarilTopLevel | SilmarilEffects, 457 | ) { 458 | // Check arguments 459 | if (path.node.arguments.length !== 1) { 460 | throw new Error(`${type} can only accept a single argument`); 461 | } 462 | const isTopLevel = type === '$$' || type === '$composable'; 463 | const arg = path.node.arguments[0]; 464 | const trueArg = 465 | unwrapNode(arg, t.isArrowFunctionExpression) 466 | || unwrapNode(arg, t.isFunctionExpression); 467 | if (!trueArg) { 468 | if (isTopLevel) { 469 | throw new Error(`${type} argument must be ArrowFunctionExpression or FunctionExpression`); 470 | } 471 | return; 472 | } 473 | if (t.isBlockStatement(trueArg.body)) { 474 | const instanceID = path.scope.generateUidIdentifier('ctx'); 475 | trueArg.body.body.unshift( 476 | t.variableDeclaration( 477 | 'let', 478 | [ 479 | t.variableDeclarator( 480 | instanceID, 481 | t.callExpression( 482 | getHookIdentifier(ctx, path, TRUE_CONTEXT), 483 | [], 484 | ), 485 | ), 486 | ], 487 | ), 488 | ); 489 | path.traverse({ 490 | CallExpression(p) { 491 | const { callee } = p.node; 492 | 493 | const trueIdentifier = unwrapNode(callee, t.isIdentifier); 494 | if (trueIdentifier) { 495 | const binding = p.scope.getBindingIdentifier(trueIdentifier.name); 496 | if (binding) { 497 | if (ctx.identifiers.$$.has(binding)) { 498 | transformSetup(ctx, p, '$$'); 499 | } 500 | if (ctx.identifiers.$composable.has(binding)) { 501 | transformSetup(ctx, p, '$composable'); 502 | } 503 | if (ctx.identifiers.$.has(binding)) { 504 | transformSetup(ctx, p, '$'); 505 | } 506 | if (ctx.identifiers.$sync.has(binding)) { 507 | transformSetup(ctx, p, '$sync'); 508 | } 509 | } 510 | } 511 | }, 512 | }); 513 | const identifiers = traverseIdentifiers(ctx, path, instanceID, trueArg); 514 | // Crawl again to re-register bindings 515 | path.scope.crawl(); 516 | // Transform all reads 517 | transformReads(ctx, path, trueArg, identifiers, instanceID); 518 | } 519 | } 520 | 521 | interface State extends PluginPass { 522 | ctx: StateContext; 523 | } 524 | 525 | export default function silmarilPlugin(): PluginObj { 526 | return { 527 | name: 'silmaril', 528 | pre() { 529 | this.ctx = { 530 | hooks: new Map(), 531 | identifiers: { 532 | $: new Set(), 533 | $$: new Set(), 534 | $composable: new Set(), 535 | $store: new Set(), 536 | $sync: new Set(), 537 | onDestroy: new Set(), 538 | onMount: new Set(), 539 | }, 540 | }; 541 | }, 542 | visitor: { 543 | ImportDeclaration(path, state) { 544 | extractImportIdentifiers(state.ctx, path); 545 | }, 546 | CallExpression(path, state) { 547 | const { callee } = path.node; 548 | 549 | const trueIdentifier = unwrapNode(callee, t.isIdentifier); 550 | if (trueIdentifier) { 551 | const binding = path.scope.getBindingIdentifier(trueIdentifier.name); 552 | if (binding) { 553 | if (state.ctx.identifiers.$$.has(binding)) { 554 | transformSetup(state.ctx, path, '$'); 555 | } 556 | if (state.ctx.identifiers.$composable.has(binding)) { 557 | transformSetup(state.ctx, path, '$composable'); 558 | } 559 | } 560 | } 561 | }, 562 | }, 563 | }; 564 | } 565 | --------------------------------------------------------------------------------