├── 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 | [](https://www.npmjs.com/package/rollup-plugin-silmaril) [](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 | 
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 | [](https://www.npmjs.com/package/vite-plugin-silmaril) [](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 | 
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 |
16 |
--------------------------------------------------------------------------------
/examples/solid/favicon.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/packages/solid/README.md:
--------------------------------------------------------------------------------
1 | # solid-silmaril
2 |
3 | > SolidJS bindings for [`silmaril`](https://github.com/lxsmnsyc/silmaril)
4 |
5 | [](https://www.npmjs.com/package/solid-silmaril) [](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 | 
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 | [](https://www.npmjs.com/package/silmaril) [](https://github.com/airbnb/javascript) [](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 | 
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 | [](https://www.npmjs.com/package/silmaril) [](https://github.com/airbnb/javascript) [](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 | 
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 |
--------------------------------------------------------------------------------