├── .gitattributes ├── .prettierignore ├── .gitignore ├── src ├── mixins │ └── index.ts ├── effects │ ├── index.ts │ ├── createStoppableEffect.ts │ ├── createDeferredEffect.test.ts │ └── createDeferredEffect.ts ├── index.ts ├── decorators │ ├── index.ts │ ├── metadata-shim.ts │ ├── reactive.ts │ ├── types.ts │ ├── untracked.ts │ ├── untracked.test.ts │ ├── component.ts │ ├── effect.ts │ ├── memo.ts │ └── signal.ts ├── signals │ ├── index.ts │ ├── createSyncedSignals.ts │ ├── syncSignals.ts │ ├── createSignalObject.ts │ ├── createSignalFunction.ts │ ├── createSignalFunction.test.ts │ └── createSignalObject.test.ts ├── example.ts └── index.test.ts ├── .prettierrc.cjs ├── dist ├── decorators │ ├── effect.test.d.ts │ ├── memo.test.d.ts │ ├── signal.test.d.ts │ ├── types.js │ ├── component.test.d.ts │ ├── untracked.test.d.ts │ ├── memo.test.d.ts.map │ ├── effect.test.d.ts.map │ ├── signal.test.d.ts.map │ ├── component.test.d.ts.map │ ├── untracked.test.d.ts.map │ ├── metadata-shim.d.ts │ ├── index.js │ ├── index.d.ts │ ├── index.d.ts.map │ ├── metadata-shim.d.ts.map │ ├── reactive.d.ts.map │ ├── signal.d.ts.map │ ├── untracked.d.ts.map │ ├── metadata-shim.js │ ├── index.js.map │ ├── reactive.js │ ├── effect.d.ts.map │ ├── reactive.d.ts │ ├── memo.d.ts.map │ ├── metadata-shim.js.map │ ├── component.d.ts.map │ ├── reactive.js.map │ ├── signal.d.ts │ ├── types.d.ts.map │ ├── types.d.ts │ ├── component.d.ts │ ├── types.js.map │ ├── untracked.d.ts │ ├── component.js │ ├── untracked.js │ ├── effect.d.ts │ ├── untracked.js.map │ ├── memo.d.ts │ ├── effect.js │ ├── memo.js │ ├── component.js.map │ └── signal.js ├── signals │ ├── memoify.test.d.ts │ ├── signalify.test.d.ts │ ├── createSignalFunction.test.d.ts │ ├── createSignalObject.test.d.ts │ ├── memoify.test.d.ts.map │ ├── signalify.test.d.ts.map │ ├── createSignalObject.test.d.ts.map │ ├── createSignalFunction.test.d.ts.map │ ├── createSyncedSignals.d.ts.map │ ├── index.d.ts.map │ ├── index.js │ ├── index.d.ts │ ├── syncSignals.d.ts.map │ ├── index.js.map │ ├── memoify.d.ts.map │ ├── createSyncedSignals.d.ts │ ├── createSignalFunction.d.ts.map │ ├── createSyncedSignals.js │ ├── createSignalObject.d.ts.map │ ├── signalify.d.ts.map │ ├── createSyncedSignals.js.map │ ├── syncSignals.d.ts │ ├── syncSignals.js │ ├── createSignalObject.js │ ├── createSignalObject.d.ts │ ├── createSignalFunction.d.ts │ ├── createSignalFunction.js │ ├── memoify.d.ts │ ├── syncSignals.js.map │ ├── createSignalObject.js.map │ ├── signalify.d.ts │ ├── createSignalFunction.js.map │ ├── createSignalFunction.test.js │ └── createSignalObject.test.js ├── mixins │ ├── Effectful.test.d.ts │ ├── index.d.ts │ ├── index.js │ ├── index.d.ts.map │ ├── Effectful.test.d.ts.map │ ├── index.js.map │ └── Effectful.d.ts.map ├── effects │ ├── createDeferredEffect.test.d.ts │ ├── index.d.ts │ ├── index.js │ ├── index.d.ts.map │ ├── createDeferredEffect.test.d.ts.map │ ├── index.js.map │ ├── createDeferredEffect.d.ts │ ├── createDeferredEffect.d.ts.map │ ├── createStoppableEffect.d.ts.map │ ├── createStoppableEffect.d.ts │ ├── createStoppableEffect.js │ ├── createStoppableEffect.js.map │ ├── createDeferredEffect.test.js │ ├── createDeferredEffect.js │ ├── createDeferredEffect.test.js.map │ └── createDeferredEffect.js.map ├── example.d.ts ├── index.d.ts.map ├── index.js ├── example.d.ts.map ├── index.d.ts ├── index.js.map ├── _state.d.ts.map ├── index.test.d.ts.map ├── _state.d.ts ├── index.test.d.ts └── example.js.map ├── tsconfig.json ├── .editorconfig ├── .npmignore ├── example └── index.html ├── .github └── workflows │ └── tests.yml ├── lume.config.cjs ├── LICENSE ├── package.json └── logo ├── Favicon_Classy.svg ├── Logo-Imagemark_Classy-Dark.svg └── Logo-Imagemark_Classy-Light.svg /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | *.log 4 | -------------------------------------------------------------------------------- /src/mixins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Effectful.js' 2 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require('@lume/cli/.prettierrc.js') 2 | -------------------------------------------------------------------------------- /dist/decorators/effect.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=effect.test.d.ts.map -------------------------------------------------------------------------------- /dist/decorators/memo.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=memo.test.d.ts.map -------------------------------------------------------------------------------- /dist/decorators/signal.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=signal.test.d.ts.map -------------------------------------------------------------------------------- /dist/decorators/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | export {}; 3 | //# sourceMappingURL=types.js.map -------------------------------------------------------------------------------- /dist/signals/memoify.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=memoify.test.d.ts.map -------------------------------------------------------------------------------- /dist/mixins/Effectful.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=Effectful.test.d.ts.map -------------------------------------------------------------------------------- /dist/mixins/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './Effectful.js'; 2 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /dist/mixins/index.js: -------------------------------------------------------------------------------- 1 | export * from './Effectful.js'; 2 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/signals/signalify.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=signalify.test.d.ts.map -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@lume/cli/config/ts.config.json" 3 | } 4 | -------------------------------------------------------------------------------- /dist/decorators/component.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=component.test.d.ts.map -------------------------------------------------------------------------------- /dist/decorators/untracked.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=untracked.test.d.ts.map -------------------------------------------------------------------------------- /dist/effects/createDeferredEffect.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=createDeferredEffect.test.d.ts.map -------------------------------------------------------------------------------- /dist/signals/createSignalFunction.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=createSignalFunction.test.d.ts.map -------------------------------------------------------------------------------- /dist/signals/createSignalObject.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=createSignalObject.test.d.ts.map -------------------------------------------------------------------------------- /src/effects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createDeferredEffect.js' 2 | export * from './createStoppableEffect.js' 3 | -------------------------------------------------------------------------------- /dist/effects/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './createDeferredEffect.js'; 2 | export * from './createStoppableEffect.js'; 3 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /dist/effects/index.js: -------------------------------------------------------------------------------- 1 | export * from './createDeferredEffect.js'; 2 | export * from './createStoppableEffect.js'; 3 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/decorators/memo.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"memo.test.d.ts","sourceRoot":"","sources":["../../src/decorators/memo.test.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/mixins/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mixins/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA"} -------------------------------------------------------------------------------- /dist/signals/memoify.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"memoify.test.d.ts","sourceRoot":"","sources":["../../src/signals/memoify.test.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/decorators/effect.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"effect.test.d.ts","sourceRoot":"","sources":["../../src/decorators/effect.test.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/decorators/signal.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"signal.test.d.ts","sourceRoot":"","sources":["../../src/decorators/signal.test.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/mixins/Effectful.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Effectful.test.d.ts","sourceRoot":"","sources":["../../src/mixins/Effectful.test.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/signals/signalify.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"signalify.test.d.ts","sourceRoot":"","sources":["../../src/signals/signalify.test.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/decorators/component.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"component.test.d.ts","sourceRoot":"","sources":["../../src/decorators/component.test.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/decorators/untracked.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"untracked.test.d.ts","sourceRoot":"","sources":["../../src/decorators/untracked.test.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/effects/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/effects/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAA;AACzC,cAAc,4BAA4B,CAAA"} -------------------------------------------------------------------------------- /dist/example.d.ts: -------------------------------------------------------------------------------- 1 | declare class Foo { 2 | foo: number; 3 | get lorem(): number; 4 | set lorem(v: number); 5 | } 6 | export { Foo }; 7 | //# sourceMappingURL=example.d.ts.map -------------------------------------------------------------------------------- /dist/signals/createSignalObject.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createSignalObject.test.d.ts","sourceRoot":"","sources":["../../src/signals/createSignalObject.test.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/effects/createDeferredEffect.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createDeferredEffect.test.d.ts","sourceRoot":"","sources":["../../src/effects/createDeferredEffect.test.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/signals/createSignalFunction.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createSignalFunction.test.d.ts","sourceRoot":"","sources":["../../src/signals/createSignalFunction.test.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/mixins/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","names":[],"sources":["../../src/mixins/index.ts"],"sourcesContent":["export * from './Effectful.js'\n"],"mappings":"AAAA,cAAc,gBAAgB","ignoreList":[]} -------------------------------------------------------------------------------- /dist/decorators/metadata-shim.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | declare global { 3 | interface SymbolConstructor { 4 | readonly metadata: unique symbol; 5 | } 6 | } 7 | //# sourceMappingURL=metadata-shim.d.ts.map -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decorators/index.js' 2 | export * from './effects/index.js' 3 | export * from './mixins/index.js' 4 | export * from './signals/index.js' 5 | 6 | export const version = '0.5.0' 7 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './component.js' 2 | export * from './effect.js' 3 | export * from './memo.js' 4 | export * from './reactive.js' 5 | export * from './signal.js' 6 | export * from './untracked.js' 7 | -------------------------------------------------------------------------------- /dist/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAElC,eAAO,MAAM,OAAO,UAAU,CAAA"} -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | export * from './decorators/index.js'; 2 | export * from './effects/index.js'; 3 | export * from './mixins/index.js'; 4 | export * from './signals/index.js'; 5 | export const version = '0.5.0'; 6 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/example.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"example.d.ts","sourceRoot":"","sources":["../src/example.ts"],"names":[],"mappings":"AAGA,cAAM,GAAG;IACA,GAAG,SAAM;IAEjB,IAAY,KAAK,WAEhB;IACD,IAAY,KAAK,CAAC,CAAC,QAAA,EAElB;CACD;AAoCD,OAAO,EAAC,GAAG,EAAC,CAAA"} -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './decorators/index.js'; 2 | export * from './effects/index.js'; 3 | export * from './mixins/index.js'; 4 | export * from './signals/index.js'; 5 | export declare const version = "0.5.0"; 6 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /dist/decorators/index.js: -------------------------------------------------------------------------------- 1 | export * from './component.js'; 2 | export * from './effect.js'; 3 | export * from './memo.js'; 4 | export * from './reactive.js'; 5 | export * from './signal.js'; 6 | export * from './untracked.js'; 7 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/signals/createSyncedSignals.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createSyncedSignals.d.ts","sourceRoot":"","sources":["../../src/signals/createSyncedSignals.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,6FAKrD"} -------------------------------------------------------------------------------- /src/signals/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createSignalFunction.js' 2 | export * from './createSignalObject.js' 3 | export * from './createSyncedSignals.js' 4 | export * from './memoify.js' 5 | export * from './signalify.js' 6 | export * from './syncSignals.js' 7 | -------------------------------------------------------------------------------- /dist/decorators/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './component.js'; 2 | export * from './effect.js'; 3 | export * from './memo.js'; 4 | export * from './reactive.js'; 5 | export * from './signal.js'; 6 | export * from './untracked.js'; 7 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /dist/decorators/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/decorators/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"} -------------------------------------------------------------------------------- /dist/signals/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/signals/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA"} -------------------------------------------------------------------------------- /dist/effects/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","names":[],"sources":["../../src/effects/index.ts"],"sourcesContent":["export * from './createDeferredEffect.js'\nexport * from './createStoppableEffect.js'\n"],"mappings":"AAAA,cAAc,2BAA2B;AACzC,cAAc,4BAA4B","ignoreList":[]} -------------------------------------------------------------------------------- /dist/decorators/metadata-shim.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"metadata-shim.d.ts","sourceRoot":"","sources":["../../src/decorators/metadata-shim.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAA;AAET,OAAO,CAAC,MAAM,CAAC;IACd,UAAU,iBAAiB;QAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,MAAM,CAAA;KAChC;CACD"} -------------------------------------------------------------------------------- /dist/effects/createDeferredEffect.d.ts: -------------------------------------------------------------------------------- 1 | import { createSignal as _createSignal, createEffect } from 'solid-js'; 2 | export declare let createSignal: typeof _createSignal; 3 | export declare const createDeferredEffect: typeof createEffect; 4 | //# sourceMappingURL=createDeferredEffect.d.ts.map -------------------------------------------------------------------------------- /dist/decorators/reactive.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../../src/decorators/reactive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAA;AAGhE;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,qBAAqB,GAAG,SAAS,GAAG,GAAG,CAE/F"} -------------------------------------------------------------------------------- /dist/signals/index.js: -------------------------------------------------------------------------------- 1 | export * from './createSignalFunction.js'; 2 | export * from './createSignalObject.js'; 3 | export * from './createSyncedSignals.js'; 4 | export * from './memoify.js'; 5 | export * from './signalify.js'; 6 | export * from './syncSignals.js'; 7 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/signals/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './createSignalFunction.js'; 2 | export * from './createSignalObject.js'; 3 | export * from './createSyncedSignals.js'; 4 | export * from './memoify.js'; 5 | export * from './signalify.js'; 6 | export * from './syncSignals.js'; 7 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /dist/decorators/signal.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/decorators/signal.ts"],"names":[],"mappings":"AAKA,OAAO,oBAAoB,CAAA;AAI3B;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,MAAM,CACrB,KAAK,EAAE,OAAO,EACd,OAAO,EACJ,0BAA0B,GAC1B,2BAA2B,GAC3B,2BAA2B,GAC3B,6BAA6B,GAC9B,GAAG,CA2GL"} -------------------------------------------------------------------------------- /dist/effects/createDeferredEffect.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createDeferredEffect.d.ts","sourceRoot":"","sources":["../../src/effects/createDeferredEffect.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,YAAY,IAAI,aAAa,EAAE,YAAY,EAAoC,MAAM,UAAU,CAAA;AAevG,eAAO,IAAI,YAAY,EAgCjB,OAAO,aAAa,CAAA;AAM1B,eAAO,MAAM,oBAAoB,EA0C3B,OAAO,YAAY,CAAA"} -------------------------------------------------------------------------------- /dist/effects/createStoppableEffect.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createStoppableEffect.d.ts","sourceRoot":"","sources":["../../src/effects/createStoppableEffect.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,MAAM,GAAG;IAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,IAAI,CAAA;CAAC,CAAA;AAE3D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,CAI5D"} -------------------------------------------------------------------------------- /dist/decorators/untracked.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"untracked.d.ts","sourceRoot":"","sources":["../../src/decorators/untracked.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAA;AAEhE,OAAO,oBAAoB,CAAA;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,qBAAqB,GAAG,SAAS,GAAG,GAAG,CAwBhG"} -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_style = tab 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.yml] 18 | indent_style = space 19 | indent_size = 4 20 | -------------------------------------------------------------------------------- /dist/decorators/metadata-shim.js: -------------------------------------------------------------------------------- 1 | // Until decorators land natively, we need this shim so that we can use 2 | // decorator metadata. https://github.com/microsoft/TypeScript/issues/53461 3 | 4 | export {}; // we don't export anything, but this denotes the file as a module to TypeScript 5 | 6 | // @ts-expect-error readonly 7 | Symbol.metadata ??= Symbol.for('Symbol.metadata'); 8 | //# sourceMappingURL=metadata-shim.js.map -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","names":["version"],"sources":["../src/index.ts"],"sourcesContent":["export * from './decorators/index.js'\nexport * from './effects/index.js'\nexport * from './mixins/index.js'\nexport * from './signals/index.js'\n\nexport const version = '0.5.0'\n"],"mappings":"AAAA,cAAc,uBAAuB;AACrC,cAAc,oBAAoB;AAClC,cAAc,mBAAmB;AACjC,cAAc,oBAAoB;AAElC,OAAO,MAAMA,OAAO,GAAG,OAAO","ignoreList":[]} -------------------------------------------------------------------------------- /dist/signals/syncSignals.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"syncSignals.d.ts","sourceRoot":"","sources":["../../src/signals/syncSignals.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC5B,OAAO,EAAE,MAAM,CAAC,EAChB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,EAC3B,OAAO,EAAE,MAAM,CAAC,EAChB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,6BAHZ,CAAC,UACC,CAAC,KAAK,IAAI,mBACZ,CAAC,UACC,CAAC,KAAK,IAAI,GA0B3B"} -------------------------------------------------------------------------------- /dist/decorators/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","names":[],"sources":["../../src/decorators/index.ts"],"sourcesContent":["export * from './component.js'\nexport * from './effect.js'\nexport * from './memo.js'\nexport * from './reactive.js'\nexport * from './signal.js'\nexport * from './untracked.js'\n"],"mappings":"AAAA,cAAc,gBAAgB;AAC9B,cAAc,aAAa;AAC3B,cAAc,WAAW;AACzB,cAAc,eAAe;AAC7B,cAAc,aAAa;AAC3B,cAAc,gBAAgB","ignoreList":[]} -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Specificty of the following rules matters. 2 | 3 | # Ignore everything, 4 | /**/* 5 | 6 | # but include these folders 7 | !/dist/**/* 8 | !/src/**/* 9 | 10 | # except for these files in the above folders. 11 | /dist/**/*.test.* 12 | /dist/tests/**/* 13 | /src/**/*.test.* 14 | /src/tests/**/* 15 | 16 | # The following won't work as you think it would. 17 | # /**/* 18 | # !/dist/**/* 19 | # !/src/**/* 20 | # /**/*.test.* 21 | -------------------------------------------------------------------------------- /dist/decorators/reactive.js: -------------------------------------------------------------------------------- 1 | import { untracked } from './untracked.js'; 2 | 3 | /** 4 | * @deprecated This is no longer needed for making signal fields work. The 5 | * only other use case was to make the constructor untracked.Use the new 6 | * `@untracked` decorator instead. This is now an alias for `@untracked`. 7 | */ 8 | export function reactive(value, context) { 9 | return untracked(value, context); 10 | } 11 | //# sourceMappingURL=reactive.js.map -------------------------------------------------------------------------------- /dist/signals/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","names":[],"sources":["../../src/signals/index.ts"],"sourcesContent":["export * from './createSignalFunction.js'\nexport * from './createSignalObject.js'\nexport * from './createSyncedSignals.js'\nexport * from './memoify.js'\nexport * from './signalify.js'\nexport * from './syncSignals.js'\n"],"mappings":"AAAA,cAAc,2BAA2B;AACzC,cAAc,yBAAyB;AACvC,cAAc,0BAA0B;AACxC,cAAc,cAAc;AAC5B,cAAc,gBAAgB;AAC9B,cAAc,kBAAkB","ignoreList":[]} -------------------------------------------------------------------------------- /dist/decorators/effect.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"effect.d.ts","sourceRoot":"","sources":["../../src/decorators/effect.ts"],"names":[],"mappings":"AAEA,OAAO,oBAAoB,CAAA;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,wBAAgB,MAAM,CACrB,KAAK,EAAE,QAAQ,GAAG,4BAA4B,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC,EACnE,OAAO,EAAE,2BAA2B,GAAG,6BAA6B,QA0BpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,QAIvC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,QAItC"} -------------------------------------------------------------------------------- /dist/decorators/reactive.d.ts: -------------------------------------------------------------------------------- 1 | import type { AnyConstructor } from 'lowclass/dist/Constructor.js'; 2 | /** 3 | * @deprecated This is no longer needed for making signal fields work. The 4 | * only other use case was to make the constructor untracked.Use the new 5 | * `@untracked` decorator instead. This is now an alias for `@untracked`. 6 | */ 7 | export declare function reactive(value: AnyConstructor, context: ClassDecoratorContext | undefined): any; 8 | //# sourceMappingURL=reactive.d.ts.map -------------------------------------------------------------------------------- /src/decorators/metadata-shim.ts: -------------------------------------------------------------------------------- 1 | // Until decorators land natively, we need this shim so that we can use 2 | // decorator metadata. https://github.com/microsoft/TypeScript/issues/53461 3 | 4 | export {} // we don't export anything, but this denotes the file as a module to TypeScript 5 | 6 | declare global { 7 | interface SymbolConstructor { 8 | readonly metadata: unique symbol 9 | } 10 | } 11 | 12 | // @ts-expect-error readonly 13 | Symbol.metadata ??= Symbol.for('Symbol.metadata') 14 | -------------------------------------------------------------------------------- /dist/signals/memoify.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"memoify.d.ts","sourceRoot":"","sources":["../../src/signals/memoify.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAY,UAAU,EAAC,MAAM,wBAAwB,CAAA;AAMjE,6BAA6B;AAC7B,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,QAEpD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;AACvE,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA"} -------------------------------------------------------------------------------- /src/decorators/reactive.ts: -------------------------------------------------------------------------------- 1 | import type {AnyConstructor} from 'lowclass/dist/Constructor.js' 2 | import {untracked} from './untracked.js' 3 | 4 | /** 5 | * @deprecated This is no longer needed for making signal fields work. The 6 | * only other use case was to make the constructor untracked.Use the new 7 | * `@untracked` decorator instead. This is now an alias for `@untracked`. 8 | */ 9 | export function reactive(value: AnyConstructor, context: ClassDecoratorContext | undefined): any { 10 | return untracked(value, context) 11 | } 12 | -------------------------------------------------------------------------------- /dist/signals/createSyncedSignals.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Useful as a shorthand for: 3 | * 4 | * ```js 5 | * const [[foo, setFoo], [bar, setBar]] = syncSignals(...createSignal(0), ...createSignal(0)) 6 | * ``` 7 | * 8 | * Example: 9 | * 10 | * ```js 11 | * const [[foo, setFoo], [bar, setBar]] = createSyncedSignals(0) 12 | * ``` 13 | */ 14 | export declare function createSyncedSignals(initialValue: T): readonly [readonly [() => T, (value: T) => void], readonly [() => T, (value: T) => void]]; 15 | //# sourceMappingURL=createSyncedSignals.d.ts.map -------------------------------------------------------------------------------- /dist/signals/createSignalFunction.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createSignalFunction.d.ts","sourceRoot":"","sources":["../../src/signals/createSignalFunction.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,UAAU,CAAA;AACpC,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,gCAAgC,CAAA;AAEjE;;;GAGG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAAC,IAAI,CAAC,CAAA;CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,KAAK,cAAc,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;AACxE,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA"} -------------------------------------------------------------------------------- /dist/decorators/memo.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"memo.d.ts","sourceRoot":"","sources":["../../src/decorators/memo.ts"],"names":[],"mappings":"AAEA,OAAO,oBAAoB,CAAA;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgGG;AACH,wBAAgB,IAAI,CACnB,KAAK,EACF,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,GAAG,CAAC,GACpB,CAAC,MAAM,GAAG,CAAC,GACX,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GACrB,CAAC,MAAM,IAAI,CAAC,GACZ,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,CAAC,GAChD,4BAA4B,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,KAAK,GAAG,CAAC,EAAE,wCAAwC;AACzG,OAAO,EACJ,2BAA2B,GAC3B,2BAA2B,GAC3B,6BAA6B,GAC7B,2BAA2B,QA+B9B"} -------------------------------------------------------------------------------- /dist/signals/createSyncedSignals.js: -------------------------------------------------------------------------------- 1 | import { createSignal } from 'solid-js'; 2 | import { syncSignals } from './syncSignals.js'; 3 | 4 | /** 5 | * Useful as a shorthand for: 6 | * 7 | * ```js 8 | * const [[foo, setFoo], [bar, setBar]] = syncSignals(...createSignal(0), ...createSignal(0)) 9 | * ``` 10 | * 11 | * Example: 12 | * 13 | * ```js 14 | * const [[foo, setFoo], [bar, setBar]] = createSyncedSignals(0) 15 | * ``` 16 | */ 17 | export function createSyncedSignals(initialValue) { 18 | return syncSignals(...createSignal(initialValue), ...createSignal(initialValue)); 19 | } 20 | //# sourceMappingURL=createSyncedSignals.js.map -------------------------------------------------------------------------------- /src/signals/createSyncedSignals.ts: -------------------------------------------------------------------------------- 1 | import {createSignal} from 'solid-js' 2 | import {syncSignals} from './syncSignals.js' 3 | 4 | /** 5 | * Useful as a shorthand for: 6 | * 7 | * ```js 8 | * const [[foo, setFoo], [bar, setBar]] = syncSignals(...createSignal(0), ...createSignal(0)) 9 | * ``` 10 | * 11 | * Example: 12 | * 13 | * ```js 14 | * const [[foo, setFoo], [bar, setBar]] = createSyncedSignals(0) 15 | * ``` 16 | */ 17 | export function createSyncedSignals(initialValue: T) { 18 | return syncSignals( 19 | ...(createSignal(initialValue) as [() => T, (v: T) => void]), 20 | ...(createSignal(initialValue) as [() => T, (v: T) => void]), 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /dist/decorators/metadata-shim.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"metadata-shim.js","names":["Symbol","metadata","for"],"sources":["../../src/decorators/metadata-shim.ts"],"sourcesContent":["// Until decorators land natively, we need this shim so that we can use\n// decorator metadata. https://github.com/microsoft/TypeScript/issues/53461\n\nexport {} // we don't export anything, but this denotes the file as a module to TypeScript\n\ndeclare global {\n\tinterface SymbolConstructor {\n\t\treadonly metadata: unique symbol\n\t}\n}\n\n// @ts-expect-error readonly\nSymbol.metadata ??= Symbol.for('Symbol.metadata')\n"],"mappings":"AAAA;AACA;;AAEA,UAAS,CAAC;;AAQV;AACAA,MAAM,CAACC,QAAQ,KAAKD,MAAM,CAACE,GAAG,CAAC,iBAAiB,CAAC","ignoreList":[]} -------------------------------------------------------------------------------- /dist/signals/createSignalObject.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createSignalObject.d.ts","sourceRoot":"","sources":["../../src/signals/createSignalObject.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,gCAAgC,CAAA;AACjE,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,gCAAgC,CAAA;AAE1D;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC9B,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACjB,6BAA6B;IAE7B,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAA;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;AACpE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAA"} -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 |

See console

18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.operating-system }} 8 | 9 | strategy: 10 | matrix: 11 | # TODO get tests working in Windows 12 | # windows-latest 13 | operating-system: [ubuntu-latest, macos-latest] 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Use Node.js latest 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: latest 21 | - name: npm install, build, and test 22 | run: | 23 | npm i 24 | npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /lume.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('@lume/cli/config/getUserConfig.js').UserConfig} */ 2 | module.exports = { 3 | useBabelForTypeScript: true, 4 | importMap: { 5 | imports: { 6 | 'lowclass/': '/node_modules/lowclass/', 7 | 'solid-js': '/node_modules/solid-js/dist/solid.js', 8 | 'solid-js/web': '/node_modules/solid-js/web/dist/web.js', 9 | 'solid-js/html': '/node_modules/solid-js/html/dist/html.js', 10 | 'solid-js/store': '/node_modules/solid-js/store/dist/store.js', 11 | '@solid-primitives/memo': '/node_modules/@solid-primitives/memo/dist/index.js', 12 | '@solid-primitives/scheduled': '/node_modules/@solid-primitives/scheduled/dist/index.js', 13 | '@solid-primitives/utils': '/node_modules/@solid-primitives/utils/dist/index.js', 14 | }, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /dist/decorators/component.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/decorators/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAC,MAAM,8BAA8B,CAAA;AACxD,OAAO,EAAmC,KAAK,GAAG,EAAqB,MAAM,UAAU,CAAA;AACvF,OAAO,oBAAoB,CAAA;AAW3B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,WAAW,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,GAAG,CA4DzF;AAED,OAAO,QAAQ,UAAU,CAAC;IACzB,UAAU,GAAG,CAAC;QAEb,UAAU,YAAY;YACrB,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,GAAG,CAAC,OAAO,CAAA;SACtD;QAGD,UAAU,yBAAyB;YAClC,SAAS,EAAE,EAAE,CAAA;SACb;KACD;CACD;AAED,MAAM,MAAM,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IACrE,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACtB,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,IAAI,CAAA;CAC5B,CAAA"} -------------------------------------------------------------------------------- /dist/mixins/Effectful.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Effectful.d.ts","sourceRoot":"","sources":["../../src/mixins/Effectful.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,KAAK,EAAmD,MAAM,UAAU,CAAA;AACrF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAA;AAIhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,cAAc,EAAE,IAAI,EAAE,CAAC;;iCAKvC,KAAK,CAAC,MAAM,IAAI,CAAC;;QAGnC;;;;WAIG;yBACc,MAAM,IAAI;;QAO3B;;;;;;;;;;;;;;;;;;;;;;;;;;;WA2BG;;QAcH;;WAEG;;QAQH;;;;;;;;;;;;;;;;;;;;;;;;WAwBG;;uBAMK,KAAK,GAAG,IAAI;yBACV,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI;iCAEX,MAAM,IAAI;;MAgC7B;;;wCAnI+B,IAAI;;QAGlC;;;;WAIG;+BACoB,IAAI;;QAO3B;;;;;;;;;;;;;;;;;;;;;;;;;;;WA2BG;;QAcH;;WAEG;;QAQH;;;;;;;;;;;;;;;;;;;;;;;;WAwBG;;uBAMK,KAAK,GAAG,IAAI;gCACH,IAAI;uCAEG,IAAI;;;;;AAyC9B;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,OAAQ,SAAQ,YAAmB;CAAG"} -------------------------------------------------------------------------------- /dist/_state.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"_state.d.ts","sourceRoot":"","sources":["../src/_state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,SAAS,EACT,UAAU,EACV,eAAe,EACf,OAAO,EACP,mBAAmB,EACnB,gBAAgB,EAChB,MAAM,uBAAuB,CAAA;AAI9B,OAAO,EAAC,OAAO,EAAC,MAAM,uBAAuB,CAAA;AAE7C,eAAO,MAAM,cAAc,mBAA0B,CAAA;AACrD,eAAO,MAAM,YAAY,mBAA0B,CAAA;AAEnD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,mBAAmB,mBAGvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,eAAe,cAU5F;AAgDD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,QAWhF;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,QAY9E;AAED,8BAA8B;AAC9B,eAAO,MAAM,SAAS,6BAAoC,CAAA;AAE1D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,QA+BhF;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,QAkB7E"} -------------------------------------------------------------------------------- /dist/decorators/reactive.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"reactive.js","names":["untracked","reactive","value","context"],"sources":["../../src/decorators/reactive.ts"],"sourcesContent":["import type {AnyConstructor} from 'lowclass/dist/Constructor.js'\nimport {untracked} from './untracked.js'\n\n/**\n * @deprecated This is no longer needed for making signal fields work. The\n * only other use case was to make the constructor untracked.Use the new\n * `@untracked` decorator instead. This is now an alias for `@untracked`.\n */\nexport function reactive(value: AnyConstructor, context: ClassDecoratorContext | undefined): any {\n\treturn untracked(value, context)\n}\n"],"mappings":"AACA,SAAQA,SAAS,QAAO,gBAAgB;;AAExC;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,QAAQA,CAACC,KAAqB,EAAEC,OAA0C,EAAO;EAChG,OAAOH,SAAS,CAACE,KAAK,EAAEC,OAAO,CAAC;AACjC","ignoreList":[]} -------------------------------------------------------------------------------- /dist/effects/createStoppableEffect.d.ts: -------------------------------------------------------------------------------- 1 | export type Effect = { 2 | stop: () => void; 3 | resume: () => void; 4 | }; 5 | /** 6 | * NOTE: Experimental 7 | * 8 | * Create a stoppable effect. 9 | * 10 | * ```js 11 | * const effect = createStoppableEffect(() => {...}) 12 | * 13 | * // ...later, stop the effect from running again. 14 | * effect.stop() 15 | * ``` 16 | * 17 | * Note, this is experimental because when inside of a parent reactive context 18 | * that is long-lived (f.e. for life time of the app), each new effect created 19 | * with this and subsequently stopped will stick around and not be GC'd until 20 | * the parent context is cleaned up (which could be never). 21 | * 22 | * Stopped effects will currently only be GC'd freely when they are created 23 | * outside of a reactive context. 24 | */ 25 | export declare function createStoppableEffect(fn: () => void): Effect; 26 | //# sourceMappingURL=createStoppableEffect.d.ts.map -------------------------------------------------------------------------------- /dist/decorators/signal.d.ts: -------------------------------------------------------------------------------- 1 | import './metadata-shim.js'; 2 | /** 3 | * @decorator 4 | * Decorate properties of a class with `@signal` to back them with Solid 5 | * signals, making them reactive. 6 | * 7 | * Related: See the Solid.js `createSignal` API for creating standalone signals. 8 | * 9 | * Example: 10 | * 11 | * ```js 12 | * import {signal} from 'classy-solid' 13 | * import {createEffect} from 'solid-js' 14 | * 15 | * class Counter { 16 | * ⁣@signal count = 0 17 | * 18 | * constructor() { 19 | * setInterval(() => this.count++, 1000) 20 | * } 21 | * } 22 | * 23 | * const counter = new Counter() 24 | * 25 | * createEffect(() => { 26 | * console.log('count:', counter.count) 27 | * }) 28 | * ``` 29 | */ 30 | export declare function signal(value: unknown, context: ClassFieldDecoratorContext | ClassGetterDecoratorContext | ClassSetterDecoratorContext | ClassAccessorDecoratorContext): any; 31 | //# sourceMappingURL=signal.d.ts.map -------------------------------------------------------------------------------- /dist/index.test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"AAMA,OAAO,CAAC,MAAM,CAAC;IACd,SAAS,MAAM,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,GAAG,CAAA;CACpC;AAGD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,EAAE,aAAa,SAAI,QAwB1F;;;;;;;;;;;;;;;;;;AAED,qBAAa,SAAU,SAAQ,cAAsB;IAK5C,CAAC,SAAI;IACL,CAAC,SAAI;IAEb,IAAI,SAAI;IACR,MAAM,SAAI;IAEV,iBAAiB;IAOjB,oBAAoB;CAGpB;;;;;;;;;;;;;;;;;;AAED,qBAAa,UAAW,SAAQ,eAAsB;IAK7C,CAAC,SAAI;IACL,CAAC,SAAI;IAEb,IAAI,SAAI;IACR,MAAM,SAAI;;IAUV,iBAAiB;IAIjB,oBAAoB;CAGpB;;;;;;;;;;;;;;;;;;AAED,qBAAa,UAAW,SAAQ,eAAsB;IAKrD,IAAI,SAAI;IACR,MAAM,SAAI;IAEF,CAAC,SAAI;IACL,CAAC,SAAI;IAEb,IAAU,GAAG,WAEZ;IAEO,GAAG;IAKX,iBAAiB;IAIjB,oBAAoB;CAGpB;AACD,qBAAa,UAAW,SAAQ,WAAW;IAK1C,IAAI,SAAI;IACR,MAAM,SAAI;IAEF,CAAC,SAAI;IACL,CAAC,SAAI;IAEb,IAAU,GAAG,WAEZ;IAEO,GAAG;IAKX,iBAAiB;IAIjB,oBAAoB;CAGpB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,OAAO,GAAG;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,QAoCpG"} -------------------------------------------------------------------------------- /dist/signals/signalify.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"signalify.d.ts","sourceRoot":"","sources":["../../src/signals/signalify.ts"],"names":[],"mappings":"AAGA,OAAO,EAAuB,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAA;AAKnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;AACzE,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AAC7E,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,CAAA;AAiCzG,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,WAE7E;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,QAGhF;AAED,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,MAAM,EACtD,GAAG,EAAE,CAAC,EACN,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAC9B,UAAU,EAAE,OAAO,EACnB,sBAAsB,UAAQ,GAC5B,IAAI,CAsEN;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,2BAI9G"} -------------------------------------------------------------------------------- /dist/signals/createSyncedSignals.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createSyncedSignals.js","names":["createSignal","syncSignals","createSyncedSignals","initialValue"],"sources":["../../src/signals/createSyncedSignals.ts"],"sourcesContent":["import {createSignal} from 'solid-js'\nimport {syncSignals} from './syncSignals.js'\n\n/**\n * Useful as a shorthand for:\n *\n * ```js\n * const [[foo, setFoo], [bar, setBar]] = syncSignals(...createSignal(0), ...createSignal(0))\n * ```\n *\n * Example:\n *\n * ```js\n * const [[foo, setFoo], [bar, setBar]] = createSyncedSignals(0)\n * ```\n */\nexport function createSyncedSignals(initialValue: T) {\n\treturn syncSignals(\n\t\t...(createSignal(initialValue) as [() => T, (v: T) => void]),\n\t\t...(createSignal(initialValue) as [() => T, (v: T) => void]),\n\t)\n}\n"],"mappings":"AAAA,SAAQA,YAAY,QAAO,UAAU;AACrC,SAAQC,WAAW,QAAO,kBAAkB;;AAE5C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,mBAAmBA,CAAIC,YAAe,EAAE;EACvD,OAAOF,WAAW,CACjB,GAAID,YAAY,CAACG,YAAY,CAA+B,EAC5D,GAAIH,YAAY,CAACG,YAAY,CAC9B,CAAC;AACF","ignoreList":[]} -------------------------------------------------------------------------------- /dist/effects/createStoppableEffect.js: -------------------------------------------------------------------------------- 1 | import { createEffect, createSignal } from 'solid-js'; 2 | /** 3 | * NOTE: Experimental 4 | * 5 | * Create a stoppable effect. 6 | * 7 | * ```js 8 | * const effect = createStoppableEffect(() => {...}) 9 | * 10 | * // ...later, stop the effect from running again. 11 | * effect.stop() 12 | * ``` 13 | * 14 | * Note, this is experimental because when inside of a parent reactive context 15 | * that is long-lived (f.e. for life time of the app), each new effect created 16 | * with this and subsequently stopped will stick around and not be GC'd until 17 | * the parent context is cleaned up (which could be never). 18 | * 19 | * Stopped effects will currently only be GC'd freely when they are created 20 | * outside of a reactive context. 21 | */ 22 | export function createStoppableEffect(fn) { 23 | const [running, setRunning] = createSignal(true); 24 | createEffect(() => running() && fn()); 25 | return { 26 | stop: () => setRunning(false), 27 | resume: () => setRunning(true) 28 | }; 29 | } 30 | //# sourceMappingURL=createStoppableEffect.js.map -------------------------------------------------------------------------------- /src/effects/createStoppableEffect.ts: -------------------------------------------------------------------------------- 1 | import {createEffect, createSignal} from 'solid-js' 2 | 3 | export type Effect = {stop: () => void; resume: () => void} 4 | 5 | /** 6 | * NOTE: Experimental 7 | * 8 | * Create a stoppable effect. 9 | * 10 | * ```js 11 | * const effect = createStoppableEffect(() => {...}) 12 | * 13 | * // ...later, stop the effect from running again. 14 | * effect.stop() 15 | * ``` 16 | * 17 | * Note, this is experimental because when inside of a parent reactive context 18 | * that is long-lived (f.e. for life time of the app), each new effect created 19 | * with this and subsequently stopped will stick around and not be GC'd until 20 | * the parent context is cleaned up (which could be never). 21 | * 22 | * Stopped effects will currently only be GC'd freely when they are created 23 | * outside of a reactive context. 24 | */ 25 | export function createStoppableEffect(fn: () => void): Effect { 26 | const [running, setRunning] = createSignal(true) 27 | createEffect(() => running() && fn()) 28 | return {stop: () => setRunning(false), resume: () => setRunning(true)} 29 | } 30 | -------------------------------------------------------------------------------- /dist/signals/syncSignals.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Syncs two signals together so that setting one signal's value updates the 3 | * other, and vice versa, without an infinite loop. 4 | * 5 | * Example: 6 | * 7 | * ```js 8 | * const [foo, setFoo] = createSignal(0) 9 | * const [bar, setBar] = createSignal(0) 10 | * 11 | * syncSignals(foo, setFoo, bar, setBar) 12 | * 13 | * createEffect(() => console.log(foo(), bar())) 14 | * 15 | * setFoo(1) // logs "1 1" 16 | * setBar(2) // logs "2 2" 17 | * ``` 18 | * 19 | * It returns the getters/setters, so it is possible to also create the signals 20 | * and sync them at once: 21 | * 22 | * ```js 23 | * const [[foo, setFoo], [bar, setBar]] = syncSignals(...createSignal(0), ...createSignal(0)) 24 | * 25 | * createEffect(() => console.log(foo(), bar())) 26 | * 27 | * setFoo(1) // logs "1 1" 28 | * setBar(2) // logs "2 2" 29 | * ``` 30 | */ 31 | export declare function syncSignals(getterA: () => T, setterA: (value: T) => void, getterB: () => T, setterB: (value: T) => void): readonly [readonly [() => T, (value: T) => void], readonly [() => T, (value: T) => void]]; 32 | //# sourceMappingURL=syncSignals.d.ts.map -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joseph Orbegoso Pea (joe@trusktr.io) 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. 22 | -------------------------------------------------------------------------------- /dist/decorators/types.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/decorators/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,8BAA8B,CAAA;AAC7D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,oCAAoC,CAAA;AAEtE,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;AAEpD,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,QAAQ,GAAG,4BAA4B,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;AAE/G,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;AAErC,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAIzD,MAAM,WAAW,QAAQ;IACxB,YAAY,EAAE,OAAO,CAAA;IACrB,IAAI,EAAE,aAAa,CAAA;CACnB;AAED,MAAM,MAAM,gBAAgB,GACzB,cAAc,GACd,oBAAoB,GACpB,eAAe,GACf,aAAa,GACb,sBAAsB,GACtB,eAAe,CAAA;AAElB,MAAM,MAAM,eAAe,GAAG,KAAK,CAAC,UAAU,CAAC,CAAA;AAE/C,MAAM,MAAM,UAAU,GAAG;IACxB,IAAI,EAAE,gBAAgB,CAAA;IACtB,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,QAAQ,CAAC,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;IAChC,KAAK,CAAC,EAAE,OAAO,CAAA;CACf,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IACjC,SAAS,EAAE,mBAAmB,CAAA;IAC9B,mBAAmB,CAAC,EAAE,eAAe,CAAA;IACrC,+BAA+B,CAAC,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;IACvG,kCAAkC,CAAC,EAAE;QAAC,CAAC,GAAG,EAAE,OAAO,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;KAAC,CAAA;CAChE,CAAA"} -------------------------------------------------------------------------------- /dist/decorators/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Constructor } from 'lowclass/dist/Constructor.js'; 2 | import type { SignalFunction } from '../signals/createSignalFunction.js'; 3 | export type AnyObject = Record; 4 | export type DecoratedValue = Constructor | Function | ClassAccessorDecoratorTarget | undefined; 5 | export type PropKey = string | symbol; 6 | export type SupportedKind = 'field' | 'getter' | 'setter'; 7 | export interface PropSpec { 8 | initialValue: unknown; 9 | kind: SupportedKind; 10 | } 11 | export type SignalOrMemoType = 'signal-field' | 'memo-auto-accessor' | 'memo-accessor' | 'memo-method' | 'effect-auto-accessor' | 'effect-method'; 12 | export type MetadataMembers = Array; 13 | export type MemberStat = { 14 | type: SignalOrMemoType; 15 | name: PropKey; 16 | applied: WeakMap; 17 | finalize?(this: AnyObject): void; 18 | value?: unknown; 19 | }; 20 | export type ClassySolidMetadata = { 21 | __proto__: ClassySolidMetadata; 22 | classySolid_members?: MetadataMembers; 23 | classySolid_getterSetterSignals?: Record> | undefined>; 24 | classySolid_getterSetterPairCounts?: { 25 | [key: PropKey]: 0 | 1 | 2; 26 | }; 27 | }; 28 | //# sourceMappingURL=types.d.ts.map -------------------------------------------------------------------------------- /dist/decorators/component.d.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from 'lowclass/dist/Constructor.js'; 2 | import { type JSX } from 'solid-js'; 3 | import './metadata-shim.js'; 4 | /** 5 | * A decorator for using classes as Solid components. 6 | * 7 | * Example: 8 | * 9 | * ```js 10 | * ⁣@component 11 | * class MyComp { 12 | * ⁣@signal last = 'none' 13 | * 14 | * onMount() { 15 | * console.log('mounted') 16 | * } 17 | * 18 | * template(props) { 19 | * // here we use `props` passed in, or the signal on `this` which is also 20 | * // treated as a prop 21 | * return

Hello, my name is {props.first} {this.last}

22 | * } 23 | * } 24 | * 25 | * render(() => ) 26 | * ``` 27 | */ 28 | export declare function component(Base: T, context?: DecoratorContext): any; 29 | declare module 'solid-js' { 30 | namespace JSX { 31 | interface ElementClass { 32 | template?(props: Record): JSX.Element; 33 | } 34 | interface ElementAttributesProperty { 35 | PropTypes: {}; 36 | } 37 | } 38 | } 39 | export type Props = Pick & { 40 | children?: JSX.Element; 41 | ref?: (component: T) => void; 42 | }; 43 | //# sourceMappingURL=component.d.ts.map -------------------------------------------------------------------------------- /dist/decorators/types.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"types.js","names":[],"sources":["../../src/decorators/types.ts"],"sourcesContent":["import type {Constructor} from 'lowclass/dist/Constructor.js'\nimport type {SignalFunction} from '../signals/createSignalFunction.js'\n\nexport type AnyObject = Record\n\nexport type DecoratedValue = Constructor | Function | ClassAccessorDecoratorTarget | undefined\n\nexport type PropKey = string | symbol\n\nexport type SupportedKind = 'field' | 'getter' | 'setter'\n\n// If we add options for `@signal` later (f.e. `@signal({equals: false})`),\n// those options can go in here too.\nexport interface PropSpec {\n\tinitialValue: unknown\n\tkind: SupportedKind\n}\n\nexport type SignalOrMemoType =\n\t| 'signal-field'\n\t| 'memo-auto-accessor'\n\t| 'memo-accessor'\n\t| 'memo-method'\n\t| 'effect-auto-accessor'\n\t| 'effect-method'\n\nexport type MetadataMembers = Array\n\nexport type MemberStat = {\n\ttype: SignalOrMemoType\n\tname: PropKey\n\tapplied: WeakMap\n\tfinalize?(this: AnyObject): void\n\tvalue?: unknown\n}\n\nexport type ClassySolidMetadata = {\n\t__proto__: ClassySolidMetadata\n\tclassySolid_members?: MetadataMembers\n\tclassySolid_getterSetterSignals?: Record> | undefined>\n\tclassySolid_getterSetterPairCounts?: {[key: PropKey]: 0 | 1 | 2}\n}\n"],"mappings":"","ignoreList":[]} -------------------------------------------------------------------------------- /src/decorators/types.ts: -------------------------------------------------------------------------------- 1 | import type {Constructor} from 'lowclass/dist/Constructor.js' 2 | import type {SignalFunction} from '../signals/createSignalFunction.js' 3 | 4 | export type AnyObject = Record 5 | 6 | export type DecoratedValue = Constructor | Function | ClassAccessorDecoratorTarget | undefined 7 | 8 | export type PropKey = string | symbol 9 | 10 | export type SupportedKind = 'field' | 'getter' | 'setter' 11 | 12 | // If we add options for `@signal` later (f.e. `@signal({equals: false})`), 13 | // those options can go in here too. 14 | export interface PropSpec { 15 | initialValue: unknown 16 | kind: SupportedKind 17 | } 18 | 19 | export type SignalOrMemoType = 20 | | 'signal-field' 21 | | 'memo-auto-accessor' 22 | | 'memo-accessor' 23 | | 'memo-method' 24 | | 'effect-auto-accessor' 25 | | 'effect-method' 26 | 27 | export type MetadataMembers = Array 28 | 29 | export type MemberStat = { 30 | type: SignalOrMemoType 31 | name: PropKey 32 | applied: WeakMap 33 | finalize?(this: AnyObject): void 34 | value?: unknown 35 | } 36 | 37 | export type ClassySolidMetadata = { 38 | __proto__: ClassySolidMetadata 39 | classySolid_members?: MetadataMembers 40 | classySolid_getterSetterSignals?: Record> | undefined> 41 | classySolid_getterSetterPairCounts?: {[key: PropKey]: 0 | 1 | 2} 42 | } 43 | -------------------------------------------------------------------------------- /src/example.ts: -------------------------------------------------------------------------------- 1 | import {signal, effect} from './index.js' 2 | import {createEffect} from 'solid-js' 3 | 4 | class Foo { 5 | @signal foo = 123 6 | 7 | @signal get lorem() { 8 | return 123 9 | } 10 | @signal set lorem(v) { 11 | v 12 | } 13 | } 14 | 15 | class Bar extends Foo { 16 | // This causes a TDZ error if it comes after bar. See "TDZ" example in signal.test.ts. 17 | // Spec ordering issue: https://github.com/tc39/proposal-decorators/issues/571 18 | #baz = 789 19 | 20 | @signal bar = 456 21 | 22 | // This would cause a TDZ error. 23 | // #baz = 789 24 | 25 | @signal get baz() { 26 | return this.#baz 27 | } 28 | @signal set baz(v) { 29 | this.#baz = v 30 | } 31 | 32 | @effect logFoo() { 33 | console.log('this.foo:', this.foo) 34 | } 35 | 36 | @effect logLorem() { 37 | console.log('this.lorem:', this.lorem) 38 | } 39 | 40 | @effect logBar() { 41 | console.log('this.bar:', this.bar) 42 | } 43 | 44 | @effect logBaz() { 45 | console.log('this.baz:', this.baz) 46 | } 47 | } 48 | 49 | export {Foo} 50 | 51 | console.log('---------') 52 | const b = new Bar() 53 | 54 | createEffect(() => { 55 | console.log('b.foo:', b.foo) 56 | }) 57 | 58 | createEffect(() => { 59 | console.log('b.lorem:', b.lorem) 60 | }) 61 | 62 | createEffect(() => { 63 | console.log('b.bar:', b.bar) 64 | }) 65 | 66 | createEffect(() => { 67 | console.log('b.baz:', b.baz) 68 | }) 69 | 70 | setInterval(() => { 71 | console.log('---') 72 | b.foo++ 73 | b.bar++ 74 | b.baz++ 75 | b.lorem++ 76 | }, 1000) 77 | -------------------------------------------------------------------------------- /dist/signals/syncSignals.js: -------------------------------------------------------------------------------- 1 | import { createComputed } from 'solid-js'; 2 | 3 | /** 4 | * Syncs two signals together so that setting one signal's value updates the 5 | * other, and vice versa, without an infinite loop. 6 | * 7 | * Example: 8 | * 9 | * ```js 10 | * const [foo, setFoo] = createSignal(0) 11 | * const [bar, setBar] = createSignal(0) 12 | * 13 | * syncSignals(foo, setFoo, bar, setBar) 14 | * 15 | * createEffect(() => console.log(foo(), bar())) 16 | * 17 | * setFoo(1) // logs "1 1" 18 | * setBar(2) // logs "2 2" 19 | * ``` 20 | * 21 | * It returns the getters/setters, so it is possible to also create the signals 22 | * and sync them at once: 23 | * 24 | * ```js 25 | * const [[foo, setFoo], [bar, setBar]] = syncSignals(...createSignal(0), ...createSignal(0)) 26 | * 27 | * createEffect(() => console.log(foo(), bar())) 28 | * 29 | * setFoo(1) // logs "1 1" 30 | * setBar(2) // logs "2 2" 31 | * ``` 32 | */ 33 | export function syncSignals(getterA, setterA, getterB, setterB) { 34 | let settingB = false; 35 | let settingA = false; 36 | createComputed( 37 | // @ts-ignore not all code paths return 38 | () => { 39 | const a = getterA(); 40 | if (settingA) return settingA = false; 41 | settingB = true; 42 | setterB(a); 43 | }); 44 | createComputed( 45 | // @ts-ignore not all code paths return 46 | () => { 47 | const b = getterB(); 48 | if (settingB) return settingB = false; 49 | settingA = true; 50 | setterA(b); 51 | }); 52 | return [[getterA, setterA], [getterB, setterB]]; 53 | } 54 | //# sourceMappingURL=syncSignals.js.map -------------------------------------------------------------------------------- /dist/signals/createSignalObject.js: -------------------------------------------------------------------------------- 1 | import { createSignal } from '../effects/createDeferredEffect.js'; 2 | 3 | /** 4 | * A signal represented as an object with .get and .set methods. 5 | */ 6 | 7 | /** 8 | * Create a Solid signal wrapped in the form of an object with `.get` and `.set` 9 | * methods for alternative usage patterns. 10 | * 11 | * ```js 12 | * let count = createSignalObject(0) // count starts at 0 13 | * count.set(1) // set the value of count to 1 14 | * count.set(count.get() + 1) // add 1 15 | * let currentValue = count.get() // read the current value 16 | * ``` 17 | * 18 | * This is more convenient for class properties than using `createSignal`. With `createSignal`: 19 | * 20 | * ```js 21 | * class Foo { 22 | * count = createSignal(0) 23 | * 24 | * increment() { 25 | * // difficult to read 26 | * this.count[1](this.count[0]() + 1) 27 | * 28 | * // also: 29 | * this.count[1](c => c + 1) 30 | * } 31 | * } 32 | * ``` 33 | * 34 | * With `createSignalObject`: 35 | * 36 | * ```js 37 | * class Foo { 38 | * count = createSignalObject(0) 39 | * 40 | * increment() { 41 | * // Easier to read 42 | * this.count.set(this.count.get() + 1) 43 | * 44 | * // also: 45 | * this.count.set(c => c + 1) 46 | * } 47 | * } 48 | * ``` 49 | * 50 | * See also `createSignalFunction` for another pattern. 51 | */ 52 | 53 | export function createSignalObject(value, options) { 54 | const [get, set] = createSignal(value, options); 55 | return { 56 | get, 57 | set 58 | }; 59 | } 60 | //# sourceMappingURL=createSignalObject.js.map -------------------------------------------------------------------------------- /dist/_state.d.ts: -------------------------------------------------------------------------------- 1 | import type { AnyObject, MemberStat, MetadataMembers, PropKey, ClassySolidMetadata, SignalOrMemoType } from './decorators/types.js'; 2 | import { Effects } from './mixins/Effectful.js'; 3 | export declare const isSignalGetter: WeakSet; 4 | export declare const isMemoGetter: WeakSet; 5 | export declare function getMembers(metadata: ClassySolidMetadata): MetadataMembers; 6 | export declare function getMemberStat(name: PropKey, type: SignalOrMemoType, members: MetadataMembers): MemberStat; 7 | export declare function signalifyIfNeeded(obj: AnyObject, name: PropKey, stat: MemberStat): void; 8 | export declare function memoifyIfNeeded(obj: AnyObject, name: PropKey, stat: MemberStat): void; 9 | /** @private internal state */ 10 | export declare const effects__: WeakMap; 11 | export declare function effectifyIfNeeded(obj: AnyObject, name: PropKey, stat: MemberStat): void; 12 | /** 13 | * This finalizes memo initialization for the members tracked, in our custom 14 | * ordering. 15 | * 16 | * This is important because memos may depend on signals or other memos, and we 17 | * cannot rely on EcmaScript decorator order, or extra initializer order alone, 18 | * because accessor and method decorators/initializers run before field 19 | * decorators no matter the order in source code (give or take some details 20 | * regarding auto accessor ordering). 21 | * 22 | * See: https://github.com/tc39/proposal-decorators/issues/566 23 | */ 24 | export declare function finalizeMembersIfLast(obj: AnyObject, members: MetadataMembers): void; 25 | //# sourceMappingURL=_state.d.ts.map -------------------------------------------------------------------------------- /dist/effects/createStoppableEffect.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createStoppableEffect.js","names":["createEffect","createSignal","createStoppableEffect","fn","running","setRunning","stop","resume"],"sources":["../../src/effects/createStoppableEffect.ts"],"sourcesContent":["import {createEffect, createSignal} from 'solid-js'\n\nexport type Effect = {stop: () => void; resume: () => void}\n\n/**\n * NOTE: Experimental\n *\n * Create a stoppable effect.\n *\n * ```js\n * const effect = createStoppableEffect(() => {...})\n *\n * // ...later, stop the effect from running again.\n * effect.stop()\n * ```\n *\n * Note, this is experimental because when inside of a parent reactive context\n * that is long-lived (f.e. for life time of the app), each new effect created\n * with this and subsequently stopped will stick around and not be GC'd until\n * the parent context is cleaned up (which could be never).\n *\n * Stopped effects will currently only be GC'd freely when they are created\n * outside of a reactive context.\n */\nexport function createStoppableEffect(fn: () => void): Effect {\n\tconst [running, setRunning] = createSignal(true)\n\tcreateEffect(() => running() && fn())\n\treturn {stop: () => setRunning(false), resume: () => setRunning(true)}\n}\n"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,YAAY,QAAO,UAAU;AAInD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,qBAAqBA,CAACC,EAAc,EAAU;EAC7D,MAAM,CAACC,OAAO,EAAEC,UAAU,CAAC,GAAGJ,YAAY,CAAC,IAAI,CAAC;EAChDD,YAAY,CAAC,MAAMI,OAAO,CAAC,CAAC,IAAID,EAAE,CAAC,CAAC,CAAC;EACrC,OAAO;IAACG,IAAI,EAAEA,CAAA,KAAMD,UAAU,CAAC,KAAK,CAAC;IAAEE,MAAM,EAAEA,CAAA,KAAMF,UAAU,CAAC,IAAI;EAAC,CAAC;AACvE","ignoreList":[]} -------------------------------------------------------------------------------- /src/signals/syncSignals.ts: -------------------------------------------------------------------------------- 1 | import {createComputed} from 'solid-js' 2 | 3 | /** 4 | * Syncs two signals together so that setting one signal's value updates the 5 | * other, and vice versa, without an infinite loop. 6 | * 7 | * Example: 8 | * 9 | * ```js 10 | * const [foo, setFoo] = createSignal(0) 11 | * const [bar, setBar] = createSignal(0) 12 | * 13 | * syncSignals(foo, setFoo, bar, setBar) 14 | * 15 | * createEffect(() => console.log(foo(), bar())) 16 | * 17 | * setFoo(1) // logs "1 1" 18 | * setBar(2) // logs "2 2" 19 | * ``` 20 | * 21 | * It returns the getters/setters, so it is possible to also create the signals 22 | * and sync them at once: 23 | * 24 | * ```js 25 | * const [[foo, setFoo], [bar, setBar]] = syncSignals(...createSignal(0), ...createSignal(0)) 26 | * 27 | * createEffect(() => console.log(foo(), bar())) 28 | * 29 | * setFoo(1) // logs "1 1" 30 | * setBar(2) // logs "2 2" 31 | * ``` 32 | */ 33 | export function syncSignals( 34 | getterA: () => T, 35 | setterA: (value: T) => void, 36 | getterB: () => T, 37 | setterB: (value: T) => void, 38 | ) { 39 | let settingB = false 40 | let settingA = false 41 | 42 | createComputed( 43 | // @ts-ignore not all code paths return 44 | () => { 45 | const a = getterA() 46 | if (settingA) return (settingA = false) 47 | settingB = true 48 | setterB(a) 49 | }, 50 | ) 51 | 52 | createComputed( 53 | // @ts-ignore not all code paths return 54 | () => { 55 | const b = getterB() 56 | if (settingB) return (settingB = false) 57 | settingA = true 58 | setterA(b) 59 | }, 60 | ) 61 | 62 | return [[getterA, setterA] as const, [getterB, setterB] as const] as const 63 | } 64 | -------------------------------------------------------------------------------- /dist/signals/createSignalObject.d.ts: -------------------------------------------------------------------------------- 1 | import type { SignalOptions } from 'solid-js/types/reactive/signal'; 2 | import type { Signal } from 'solid-js/types/reactive/signal'; 3 | /** 4 | * A signal represented as an object with .get and .set methods. 5 | */ 6 | export interface SignalObject { 7 | /** Gets the signal value. */ 8 | get: Signal[0]; 9 | /** Sets the signal value. */ 10 | set: (v: T | ((prev: T) => T)) => T; 11 | } 12 | /** 13 | * Create a Solid signal wrapped in the form of an object with `.get` and `.set` 14 | * methods for alternative usage patterns. 15 | * 16 | * ```js 17 | * let count = createSignalObject(0) // count starts at 0 18 | * count.set(1) // set the value of count to 1 19 | * count.set(count.get() + 1) // add 1 20 | * let currentValue = count.get() // read the current value 21 | * ``` 22 | * 23 | * This is more convenient for class properties than using `createSignal`. With `createSignal`: 24 | * 25 | * ```js 26 | * class Foo { 27 | * count = createSignal(0) 28 | * 29 | * increment() { 30 | * // difficult to read 31 | * this.count[1](this.count[0]() + 1) 32 | * 33 | * // also: 34 | * this.count[1](c => c + 1) 35 | * } 36 | * } 37 | * ``` 38 | * 39 | * With `createSignalObject`: 40 | * 41 | * ```js 42 | * class Foo { 43 | * count = createSignalObject(0) 44 | * 45 | * increment() { 46 | * // Easier to read 47 | * this.count.set(this.count.get() + 1) 48 | * 49 | * // also: 50 | * this.count.set(c => c + 1) 51 | * } 52 | * } 53 | * ``` 54 | * 55 | * See also `createSignalFunction` for another pattern. 56 | */ 57 | export declare function createSignalObject(): SignalObject; 58 | export declare function createSignalObject(value: T, options?: SignalOptions): SignalObject; 59 | //# sourceMappingURL=createSignalObject.d.ts.map -------------------------------------------------------------------------------- /dist/signals/createSignalFunction.d.ts: -------------------------------------------------------------------------------- 1 | import type { Setter } from 'solid-js'; 2 | import type { SignalOptions } from 'solid-js/types/reactive/signal'; 3 | /** 4 | * A single function that with no args passed reads a signal, otherwise sets a 5 | * signal just like a Setter does. 6 | */ 7 | export type SignalFunction = { 8 | (): T; 9 | } & Setter; 10 | /** 11 | * Create a Solid signal wrapped as a single function that gets the value when 12 | * no arguments are passed in, and sets the value when an argument is passed in. 13 | * Good for alternative usage patterns, such as when read/write segregation is 14 | * not needed. 15 | * 16 | * ```js 17 | * let count = createSignalFunction(0) // create it with default value 18 | * count(1) // set the value 19 | * count(count() + 1) // increment 20 | * let currentValue = count() // read the current value 21 | * ``` 22 | * 23 | * This is more convenient for class properties than using `createSignal`. With `createSignal`: 24 | * 25 | * ```js 26 | * class Foo { 27 | * count = createSignal(0) 28 | * 29 | * increment() { 30 | * // difficult to read 31 | * this.count[1](this.count[0]() + 1) 32 | * 33 | * // also: 34 | * this.count[1](c => c + 1) 35 | * } 36 | * } 37 | * ``` 38 | * 39 | * With `createSignalFunction`: 40 | * 41 | * ```js 42 | * class Foo { 43 | * count = createSignalFunction(0) 44 | * 45 | * increment() { 46 | * // Easier to read 47 | * this.count(this.count() + 1) 48 | * 49 | * // also: 50 | * this.count(c => c + 1) 51 | * } 52 | * } 53 | * ``` 54 | * 55 | * See also `createSignalObject` for another pattern. 56 | */ 57 | export declare function createSignalFunction(): SignalFunction; 58 | export declare function createSignalFunction(value: T, options?: SignalOptions): SignalFunction; 59 | //# sourceMappingURL=createSignalFunction.d.ts.map -------------------------------------------------------------------------------- /src/signals/createSignalObject.ts: -------------------------------------------------------------------------------- 1 | import {createSignal} from '../effects/createDeferredEffect.js' 2 | import type {SignalOptions} from 'solid-js/types/reactive/signal' 3 | import type {Signal} from 'solid-js/types/reactive/signal' 4 | 5 | /** 6 | * A signal represented as an object with .get and .set methods. 7 | */ 8 | export interface SignalObject { 9 | /** Gets the signal value. */ 10 | get: Signal[0] 11 | /** Sets the signal value. */ 12 | // set: Signal[1] // FIXME broke in Solid 1.7.9 13 | set: (v: T | ((prev: T) => T)) => T 14 | } 15 | 16 | /** 17 | * Create a Solid signal wrapped in the form of an object with `.get` and `.set` 18 | * methods for alternative usage patterns. 19 | * 20 | * ```js 21 | * let count = createSignalObject(0) // count starts at 0 22 | * count.set(1) // set the value of count to 1 23 | * count.set(count.get() + 1) // add 1 24 | * let currentValue = count.get() // read the current value 25 | * ``` 26 | * 27 | * This is more convenient for class properties than using `createSignal`. With `createSignal`: 28 | * 29 | * ```js 30 | * class Foo { 31 | * count = createSignal(0) 32 | * 33 | * increment() { 34 | * // difficult to read 35 | * this.count[1](this.count[0]() + 1) 36 | * 37 | * // also: 38 | * this.count[1](c => c + 1) 39 | * } 40 | * } 41 | * ``` 42 | * 43 | * With `createSignalObject`: 44 | * 45 | * ```js 46 | * class Foo { 47 | * count = createSignalObject(0) 48 | * 49 | * increment() { 50 | * // Easier to read 51 | * this.count.set(this.count.get() + 1) 52 | * 53 | * // also: 54 | * this.count.set(c => c + 1) 55 | * } 56 | * } 57 | * ``` 58 | * 59 | * See also `createSignalFunction` for another pattern. 60 | */ 61 | export function createSignalObject(): SignalObject 62 | export function createSignalObject(value: T, options?: SignalOptions): SignalObject 63 | export function createSignalObject(value?: T, options?: SignalOptions): SignalObject { 64 | const [get, set] = createSignal(value as T, options) 65 | return {get, set} 66 | } 67 | -------------------------------------------------------------------------------- /dist/signals/createSignalFunction.js: -------------------------------------------------------------------------------- 1 | // FIXME Solid 1.7.9+ requires a TypeScript update, so classy-solid code is made 2 | // un-typesafe until we update. 3 | 4 | // import {createSignal} from './createDeferredEffect.js' 5 | import { createSignal } from 'solid-js'; 6 | 7 | /** 8 | * A single function that with no args passed reads a signal, otherwise sets a 9 | * signal just like a Setter does. 10 | */ 11 | // FIXME broke in 1.7.9 12 | 13 | /** 14 | * Create a Solid signal wrapped as a single function that gets the value when 15 | * no arguments are passed in, and sets the value when an argument is passed in. 16 | * Good for alternative usage patterns, such as when read/write segregation is 17 | * not needed. 18 | * 19 | * ```js 20 | * let count = createSignalFunction(0) // create it with default value 21 | * count(1) // set the value 22 | * count(count() + 1) // increment 23 | * let currentValue = count() // read the current value 24 | * ``` 25 | * 26 | * This is more convenient for class properties than using `createSignal`. With `createSignal`: 27 | * 28 | * ```js 29 | * class Foo { 30 | * count = createSignal(0) 31 | * 32 | * increment() { 33 | * // difficult to read 34 | * this.count[1](this.count[0]() + 1) 35 | * 36 | * // also: 37 | * this.count[1](c => c + 1) 38 | * } 39 | * } 40 | * ``` 41 | * 42 | * With `createSignalFunction`: 43 | * 44 | * ```js 45 | * class Foo { 46 | * count = createSignalFunction(0) 47 | * 48 | * increment() { 49 | * // Easier to read 50 | * this.count(this.count() + 1) 51 | * 52 | * // also: 53 | * this.count(c => c + 1) 54 | * } 55 | * } 56 | * ``` 57 | * 58 | * See also `createSignalObject` for another pattern. 59 | */ 60 | 61 | export function createSignalFunction(value, options) { 62 | const [get, set] = createSignal(value, options); 63 | return function (value) { 64 | if (arguments.length === 0) return get(); 65 | return set( 66 | // @ts-ignore FIXME its ok, value is defined (even if `undefined`) 67 | value); 68 | }; 69 | } 70 | //# sourceMappingURL=createSignalFunction.js.map -------------------------------------------------------------------------------- /dist/decorators/untracked.d.ts: -------------------------------------------------------------------------------- 1 | import type { AnyConstructor } from 'lowclass/dist/Constructor.js'; 2 | import './metadata-shim.js'; 3 | /** 4 | * A decorator that makes a class's contructor untracked. 5 | * 6 | * Sometimes, not typically, you may want to ensure that when a class is 7 | * instantiated, any signal reads that happen during the constructor do not 8 | * track those reads. 9 | * 10 | * Normally you do not need to read signals during construction, but if you do, 11 | * you should use `@untracked` to avoid accidentally creating dependencies on 12 | * those signals for any effects that instantiate the class (therefore avoiding 13 | * infinite loops). 14 | * 15 | * Example: 16 | * 17 | * ```ts 18 | * import {untracked, signal} from "classy-solid"; 19 | * import {createEffect} from "solid-js"; 20 | * 21 | * ⁣@untracked 22 | * class Example { 23 | * ⁣@signal count = 0; 24 | * 25 | * constructor() { 26 | * this.count = this.count + 1; // does not track .count signal read in any outer effect. 27 | * } 28 | * } 29 | * 30 | * createEffect(() => { 31 | * // This does not track .count, so this effect will not re-run when .count changes. 32 | * // If this did track .count, an infinite loop would happen. 33 | * const example = new Example(); 34 | * 35 | * createEffect(() => { 36 | * // This inner effect tracks .count, so it will re-run (independent of the 37 | * // outer effect) when .count changes. 38 | * console.log(example.count); 39 | * }); 40 | * }); 41 | * ``` 42 | * 43 | * This can also be called manually without decorators: 44 | * 45 | * ```ts 46 | * import {untracked} from "classy-solid"; 47 | * 48 | * const Example = untracked( 49 | * class { 50 | * count = 0; 51 | * 52 | * constructor() { 53 | * this.count = this.count + 1; // does not track .count signal read in any outer effect. 54 | * } 55 | * } 56 | * ) 57 | * 58 | * // ...same usage as above... 59 | * ``` 60 | */ 61 | export declare function untracked(value: AnyConstructor, context: ClassDecoratorContext | undefined): any; 62 | //# sourceMappingURL=untracked.d.ts.map -------------------------------------------------------------------------------- /dist/signals/memoify.d.ts: -------------------------------------------------------------------------------- 1 | import type { MemberStat } from '../decorators/types.js'; 2 | /** @private internal only */ 3 | export declare function setMemoifyMemberStat(stat: MemberStat): void; 4 | /** 5 | * Convert properties on an object into Solid.js memoized properties. 6 | * 7 | * There are two ways to use this: 8 | * 9 | * 1. Define which properties to convert to memoized properties by providing 10 | * property names as trailing arguments. Properties that are not function-valued 11 | * or accessors will be ignored. 12 | * 2. If no property names are provided, all function-valued properties and 13 | * accessors on the object will be automatically converted to memoized 14 | * properties. 15 | * 16 | * If any property is already memoified with `memoify()`, or already signalified 17 | * with `signalify()`, it will be skipped. 18 | * 19 | * Example with a plain object: 20 | * 21 | * ```js 22 | * import {memoify, signalify} from 'classy-solid' 23 | * import {createEffect} from 'solid-js' 24 | * 25 | * const obj = { 26 | * a: 1, 27 | * b: 2, 28 | * get sum() { 29 | * return this.a + this.b 30 | * } 31 | * } 32 | * 33 | * signalify(obj, 'a', 'b') 34 | * memoify(obj, 'sum') 35 | * 36 | * createEffect(() => { 37 | * console.log('sum:', obj.sum) 38 | * }) 39 | * 40 | * obj.a = 3 // updates sum to 5 41 | * ``` 42 | * 43 | * Example with a class: 44 | * 45 | * ```js 46 | * import {memoify, signalify} from 'classy-solid' 47 | * import {createEffect} from 'solid-js' 48 | * 49 | * class Example { 50 | * a = 1 51 | * b = 2 52 | * 53 | * get sum() { 54 | * return this.a + this.b 55 | * } 56 | * 57 | * constructor() { 58 | * signalify(this, 'a', 'b') 59 | * memoify(this, 'sum') 60 | * } 61 | * } 62 | * 63 | * const ex = new Example() 64 | * 65 | * createEffect(() => { 66 | * console.log('sum:', ex.sum) 67 | * }) 68 | * 69 | * ex.a = 3 // updates sum to 5 70 | * ``` 71 | */ 72 | export declare function memoify(obj: T): T; 73 | export declare function memoify(obj: T, ...props: (keyof T)[]): T; 74 | //# sourceMappingURL=memoify.d.ts.map -------------------------------------------------------------------------------- /src/effects/createDeferredEffect.test.ts: -------------------------------------------------------------------------------- 1 | import {createRoot} from 'solid-js' 2 | import {createSignalFunction} from '../signals/createSignalFunction.js' 3 | import {createDeferredEffect} from './createDeferredEffect.js' 4 | 5 | describe('classy-solid', () => { 6 | describe('createDeferredEffect()', () => { 7 | it('works', async () => { 8 | const count = createSignalFunction(0) 9 | const foo = createSignalFunction(0) 10 | 11 | let runCount = 0 12 | 13 | const stop = (() => { 14 | let stop!: () => void 15 | 16 | createRoot(_stop => { 17 | stop = _stop 18 | 19 | // Runs once initially after the current root context just 20 | // like createEffect, then any time it re-runs due to a 21 | // change in a dependency, the re-run will be deferred in 22 | // the next microtask and will run only once (not once per 23 | // signal that changed) 24 | createDeferredEffect(() => { 25 | count() 26 | foo() 27 | runCount++ 28 | }) 29 | }) 30 | 31 | return stop 32 | })() 33 | 34 | // Queues the effect to run in the next microtask 35 | count(1) 36 | count(2) 37 | foo(3) 38 | 39 | // Still 1 because the deferred effect didn't run yet, it will in the next microtask. 40 | expect(runCount).toBe(1) 41 | 42 | await Promise.resolve() 43 | 44 | // It ran only once in the previous microtask (batched), not once per signal write. 45 | expect(runCount).toBe(2) 46 | 47 | count(3) 48 | count(4) 49 | foo(5) 50 | 51 | expect(runCount).toBe(2) 52 | 53 | await Promise.resolve() 54 | 55 | expect(runCount).toBe(3) 56 | 57 | // Stops the effect from re-running. It can now be garbage collected. 58 | stop() 59 | 60 | count(3) 61 | count(4) 62 | foo(5) 63 | 64 | expect(runCount).toBe(3) 65 | 66 | await Promise.resolve() 67 | 68 | // Still the same because it was stopped, so it didn't run in the 69 | // macrotask prior to the await. 70 | expect(runCount).toBe(3) 71 | 72 | // Double check just in case (the wrong implementation would make it 73 | // skip two microtasks before running). 74 | await Promise.resolve() 75 | expect(runCount).toBe(3) 76 | }) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /dist/effects/createDeferredEffect.test.js: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'solid-js'; 2 | import { createSignalFunction } from '../signals/createSignalFunction.js'; 3 | import { createDeferredEffect } from './createDeferredEffect.js'; 4 | describe('classy-solid', () => { 5 | describe('createDeferredEffect()', () => { 6 | it('works', async () => { 7 | const count = createSignalFunction(0); 8 | const foo = createSignalFunction(0); 9 | let runCount = 0; 10 | const stop = (() => { 11 | let stop; 12 | createRoot(_stop => { 13 | stop = _stop; 14 | 15 | // Runs once initially after the current root context just 16 | // like createEffect, then any time it re-runs due to a 17 | // change in a dependency, the re-run will be deferred in 18 | // the next microtask and will run only once (not once per 19 | // signal that changed) 20 | createDeferredEffect(() => { 21 | count(); 22 | foo(); 23 | runCount++; 24 | }); 25 | }); 26 | return stop; 27 | })(); 28 | 29 | // Queues the effect to run in the next microtask 30 | count(1); 31 | count(2); 32 | foo(3); 33 | 34 | // Still 1 because the deferred effect didn't run yet, it will in the next microtask. 35 | expect(runCount).toBe(1); 36 | await Promise.resolve(); 37 | 38 | // It ran only once in the previous microtask (batched), not once per signal write. 39 | expect(runCount).toBe(2); 40 | count(3); 41 | count(4); 42 | foo(5); 43 | expect(runCount).toBe(2); 44 | await Promise.resolve(); 45 | expect(runCount).toBe(3); 46 | 47 | // Stops the effect from re-running. It can now be garbage collected. 48 | stop(); 49 | count(3); 50 | count(4); 51 | foo(5); 52 | expect(runCount).toBe(3); 53 | await Promise.resolve(); 54 | 55 | // Still the same because it was stopped, so it didn't run in the 56 | // macrotask prior to the await. 57 | expect(runCount).toBe(3); 58 | 59 | // Double check just in case (the wrong implementation would make it 60 | // skip two microtasks before running). 61 | await Promise.resolve(); 62 | expect(runCount).toBe(3); 63 | }); 64 | }); 65 | }); 66 | //# sourceMappingURL=createDeferredEffect.test.js.map -------------------------------------------------------------------------------- /dist/signals/syncSignals.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"syncSignals.js","names":["createComputed","syncSignals","getterA","setterA","getterB","setterB","settingB","settingA","a","b"],"sources":["../../src/signals/syncSignals.ts"],"sourcesContent":["import {createComputed} from 'solid-js'\n\n/**\n * Syncs two signals together so that setting one signal's value updates the\n * other, and vice versa, without an infinite loop.\n *\n * Example:\n *\n * ```js\n * const [foo, setFoo] = createSignal(0)\n * const [bar, setBar] = createSignal(0)\n *\n * syncSignals(foo, setFoo, bar, setBar)\n *\n * createEffect(() => console.log(foo(), bar()))\n *\n * setFoo(1) // logs \"1 1\"\n * setBar(2) // logs \"2 2\"\n * ```\n *\n * It returns the getters/setters, so it is possible to also create the signals\n * and sync them at once:\n *\n * ```js\n * const [[foo, setFoo], [bar, setBar]] = syncSignals(...createSignal(0), ...createSignal(0))\n *\n * createEffect(() => console.log(foo(), bar()))\n *\n * setFoo(1) // logs \"1 1\"\n * setBar(2) // logs \"2 2\"\n * ```\n */\nexport function syncSignals(\n\tgetterA: () => T,\n\tsetterA: (value: T) => void,\n\tgetterB: () => T,\n\tsetterB: (value: T) => void,\n) {\n\tlet settingB = false\n\tlet settingA = false\n\n\tcreateComputed(\n\t\t// @ts-ignore not all code paths return\n\t\t() => {\n\t\t\tconst a = getterA()\n\t\t\tif (settingA) return (settingA = false)\n\t\t\tsettingB = true\n\t\t\tsetterB(a)\n\t\t},\n\t)\n\n\tcreateComputed(\n\t\t// @ts-ignore not all code paths return\n\t\t() => {\n\t\t\tconst b = getterB()\n\t\t\tif (settingB) return (settingB = false)\n\t\t\tsettingA = true\n\t\t\tsetterA(b)\n\t\t},\n\t)\n\n\treturn [[getterA, setterA] as const, [getterB, setterB] as const] as const\n}\n"],"mappings":"AAAA,SAAQA,cAAc,QAAO,UAAU;;AAEvC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,WAAWA,CAC1BC,OAAgB,EAChBC,OAA2B,EAC3BC,OAAgB,EAChBC,OAA2B,EAC1B;EACD,IAAIC,QAAQ,GAAG,KAAK;EACpB,IAAIC,QAAQ,GAAG,KAAK;EAEpBP,cAAc;EACb;EACA,MAAM;IACL,MAAMQ,CAAC,GAAGN,OAAO,CAAC,CAAC;IACnB,IAAIK,QAAQ,EAAE,OAAQA,QAAQ,GAAG,KAAK;IACtCD,QAAQ,GAAG,IAAI;IACfD,OAAO,CAACG,CAAC,CAAC;EACX,CACD,CAAC;EAEDR,cAAc;EACb;EACA,MAAM;IACL,MAAMS,CAAC,GAAGL,OAAO,CAAC,CAAC;IACnB,IAAIE,QAAQ,EAAE,OAAQA,QAAQ,GAAG,KAAK;IACtCC,QAAQ,GAAG,IAAI;IACfJ,OAAO,CAACM,CAAC,CAAC;EACX,CACD,CAAC;EAED,OAAO,CAAC,CAACP,OAAO,EAAEC,OAAO,CAAC,EAAW,CAACC,OAAO,EAAEC,OAAO,CAAC,CAAU;AAClE","ignoreList":[]} -------------------------------------------------------------------------------- /src/signals/createSignalFunction.ts: -------------------------------------------------------------------------------- 1 | // FIXME Solid 1.7.9+ requires a TypeScript update, so classy-solid code is made 2 | // un-typesafe until we update. 3 | 4 | // import {createSignal} from './createDeferredEffect.js' 5 | import {createSignal} from 'solid-js' 6 | import type {Setter} from 'solid-js' 7 | import type {SignalOptions} from 'solid-js/types/reactive/signal' 8 | 9 | /** 10 | * A single function that with no args passed reads a signal, otherwise sets a 11 | * signal just like a Setter does. 12 | */ 13 | export type SignalFunction = {(): T} & Setter // FIXME broke in 1.7.9 14 | 15 | /** 16 | * Create a Solid signal wrapped as a single function that gets the value when 17 | * no arguments are passed in, and sets the value when an argument is passed in. 18 | * Good for alternative usage patterns, such as when read/write segregation is 19 | * not needed. 20 | * 21 | * ```js 22 | * let count = createSignalFunction(0) // create it with default value 23 | * count(1) // set the value 24 | * count(count() + 1) // increment 25 | * let currentValue = count() // read the current value 26 | * ``` 27 | * 28 | * This is more convenient for class properties than using `createSignal`. With `createSignal`: 29 | * 30 | * ```js 31 | * class Foo { 32 | * count = createSignal(0) 33 | * 34 | * increment() { 35 | * // difficult to read 36 | * this.count[1](this.count[0]() + 1) 37 | * 38 | * // also: 39 | * this.count[1](c => c + 1) 40 | * } 41 | * } 42 | * ``` 43 | * 44 | * With `createSignalFunction`: 45 | * 46 | * ```js 47 | * class Foo { 48 | * count = createSignalFunction(0) 49 | * 50 | * increment() { 51 | * // Easier to read 52 | * this.count(this.count() + 1) 53 | * 54 | * // also: 55 | * this.count(c => c + 1) 56 | * } 57 | * } 58 | * ``` 59 | * 60 | * See also `createSignalObject` for another pattern. 61 | */ 62 | export function createSignalFunction(): SignalFunction 63 | export function createSignalFunction(value: T, options?: SignalOptions): SignalFunction 64 | export function createSignalFunction(value?: T, options?: SignalOptions): SignalFunction { 65 | const [get, set] = createSignal(value as T, options) 66 | 67 | return function (value) { 68 | if (arguments.length === 0) return get() 69 | return set( 70 | // @ts-ignore FIXME its ok, value is defined (even if `undefined`) 71 | value, 72 | ) 73 | } as SignalFunction 74 | } 75 | -------------------------------------------------------------------------------- /dist/signals/createSignalObject.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createSignalObject.js","names":["createSignal","createSignalObject","value","options","get","set"],"sources":["../../src/signals/createSignalObject.ts"],"sourcesContent":["import {createSignal} from '../effects/createDeferredEffect.js'\nimport type {SignalOptions} from 'solid-js/types/reactive/signal'\nimport type {Signal} from 'solid-js/types/reactive/signal'\n\n/**\n * A signal represented as an object with .get and .set methods.\n */\nexport interface SignalObject {\n\t/** Gets the signal value. */\n\tget: Signal[0]\n\t/** Sets the signal value. */\n\t// set: Signal[1] // FIXME broke in Solid 1.7.9\n\tset: (v: T | ((prev: T) => T)) => T\n}\n\n/**\n * Create a Solid signal wrapped in the form of an object with `.get` and `.set`\n * methods for alternative usage patterns.\n *\n * ```js\n * let count = createSignalObject(0) // count starts at 0\n * count.set(1) // set the value of count to 1\n * count.set(count.get() + 1) // add 1\n * let currentValue = count.get() // read the current value\n * ```\n *\n * This is more convenient for class properties than using `createSignal`. With `createSignal`:\n *\n * ```js\n * class Foo {\n * count = createSignal(0)\n *\n * increment() {\n * // difficult to read\n * this.count[1](this.count[0]() + 1)\n *\n * // also:\n * this.count[1](c => c + 1)\n * }\n * }\n * ```\n *\n * With `createSignalObject`:\n *\n * ```js\n * class Foo {\n * count = createSignalObject(0)\n *\n * increment() {\n * // Easier to read\n * this.count.set(this.count.get() + 1)\n *\n * // also:\n * this.count.set(c => c + 1)\n * }\n * }\n * ```\n *\n * See also `createSignalFunction` for another pattern.\n */\nexport function createSignalObject(): SignalObject\nexport function createSignalObject(value: T, options?: SignalOptions): SignalObject\nexport function createSignalObject(value?: T, options?: SignalOptions): SignalObject {\n\tconst [get, set] = createSignal(value as T, options)\n\treturn {get, set}\n}\n"],"mappings":"AAAA,SAAQA,YAAY,QAAO,oCAAoC;;AAI/D;AACA;AACA;;AASA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA,OAAO,SAASC,kBAAkBA,CAAIC,KAAS,EAAEC,OAA0B,EAAmB;EAC7F,MAAM,CAACC,GAAG,EAAEC,GAAG,CAAC,GAAGL,YAAY,CAAIE,KAAK,EAAOC,OAAO,CAAC;EACvD,OAAO;IAACC,GAAG;IAAEC;EAAG,CAAC;AAClB","ignoreList":[]} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "classy-solid", 3 | "version": "0.5.0", 4 | "description": "Solid.js reactivity patterns for classes, and class components.", 5 | "author": "Joe Pea ", 6 | "contributors": [ 7 | { 8 | "name": "Joe Pea", 9 | "email": "trusktr@gmail.com", 10 | "url": "https://lume.io" 11 | } 12 | ], 13 | "license": "MIT", 14 | "homepage": "http://github.com/lume/classy-solid#readme", 15 | "type": "module", 16 | "main": "dist/index.js", 17 | "// main": "The 'main' field is fallback for legacy Node.js that has no type:module support, otherwise Node 13.2+ ignores it if type:module is set.", 18 | "types": "dist/index.d.ts", 19 | "scripts": { 20 | "LUME SCRIPTS XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX": "", 21 | "clean": "lume clean", 22 | "build": "lume build", 23 | "dev": "lume dev", 24 | "typecheck": "lume typecheck", 25 | "typecheck:watch": "lume typecheckWatch", 26 | "test": "lume test", 27 | "test:watch": "lume test --watch", 28 | "prettier": "lume prettier", 29 | "prettier:check": "lume prettierCheck", 30 | "release:patch": "lume releasePatch", 31 | "release:minor": "lume releaseMinor", 32 | "release:major": "lume releaseMajor", 33 | "version": "lume versionHook", 34 | "postversion": "lume postVersionHook", 35 | "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX OTHER SCRIPTS": "", 36 | "example": "five-server . --open=example" 37 | }, 38 | "dependencies": { 39 | "@solid-primitives/memo": "^1.4.3", 40 | "lowclass": "^8.0.0", 41 | "solid-js": "^1.3.13" 42 | }, 43 | "devDependencies": { 44 | "@lume/cli": "^0.14.0", 45 | "five-server": "^0.3.1", 46 | "prettier": "3.0.3", 47 | "typescript": "^5.0.0" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "git+ssh://git@github.com/lume/classy-solid.git" 52 | }, 53 | "bugs": { 54 | "url": "https://github.com/lume/classy-solid/issues" 55 | }, 56 | "keywords": [ 57 | "solidhack", 58 | "best_ecosystem", 59 | "components", 60 | "dep tracking reactivity", 61 | "dep-tracking-reactivity", 62 | "lume", 63 | "reactive coding", 64 | "reactive computation", 65 | "reactive programming", 66 | "reactive variables", 67 | "reactive", 68 | "reactive-coding", 69 | "reactive-computation", 70 | "reactive-programming", 71 | "reactive-variables", 72 | "reactive-effects", 73 | "reactivity", 74 | "solid", 75 | "solid-js", 76 | "solid.js", 77 | "solidjs", 78 | "true-reactivity", 79 | "ts", 80 | "typescript", 81 | "web-components" 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /dist/decorators/component.js: -------------------------------------------------------------------------------- 1 | import { Constructor } from 'lowclass/dist/Constructor.js'; 2 | import { onMount, createEffect, onCleanup, $TRACK, createMemo } from 'solid-js'; 3 | import './metadata-shim.js'; 4 | 5 | // https://github.com/ryansolid/dom-expressions/pull/122 6 | 7 | /** 8 | * A decorator for using classes as Solid components. 9 | * 10 | * Example: 11 | * 12 | * ```js 13 | * ⁣@component 14 | * class MyComp { 15 | * ⁣@signal last = 'none' 16 | * 17 | * onMount() { 18 | * console.log('mounted') 19 | * } 20 | * 21 | * template(props) { 22 | * // here we use `props` passed in, or the signal on `this` which is also 23 | * // treated as a prop 24 | * return

Hello, my name is {props.first} {this.last}

25 | * } 26 | * } 27 | * 28 | * render(() => ) 29 | * ``` 30 | */ 31 | export function component(Base, context) { 32 | if (typeof Base !== 'function' || context && context.kind !== 'class') throw new Error('The @component decorator should only be used on a class.'); 33 | const Class = Constructor(Base); 34 | 35 | // Solid only undetstands function components, so we create a wrapper 36 | // function that instantiates the class and hooks up lifecycle methods and 37 | // props. 38 | function classComponentWrapper(props) { 39 | const instance = new Class(); 40 | const keys = createMemo(() => { 41 | props[$TRACK]; 42 | return Object.keys(props); 43 | }, [], { 44 | equals(prev, next) { 45 | if (prev.length !== next.length) return false; 46 | for (let i = 0, l = prev.length; i < l; i += 1) if (prev[i] !== next[i]) return false; 47 | return true; 48 | } 49 | }); 50 | createEffect(() => { 51 | // @ts-expect-error index signature 52 | for (const prop of keys()) createEffect(() => instance[prop] = props[prop]); 53 | }); 54 | onMount(() => { 55 | instance.onMount?.(); 56 | createEffect(() => { 57 | const ref = props.ref; 58 | ref?.(instance); 59 | }); 60 | onCleanup(() => instance.onCleanup?.()); 61 | }); 62 | return instance.template?.(props) ?? null; 63 | } 64 | Object.defineProperties(classComponentWrapper, { 65 | name: { 66 | value: Class.name, 67 | configurable: true 68 | }, 69 | [Symbol.hasInstance]: { 70 | value(obj) { 71 | return obj instanceof Class; 72 | }, 73 | configurable: true 74 | } 75 | }); 76 | return classComponentWrapper; 77 | } 78 | //# sourceMappingURL=component.js.map -------------------------------------------------------------------------------- /dist/signals/signalify.d.ts: -------------------------------------------------------------------------------- 1 | import { type SignalFunction } from './createSignalFunction.js'; 2 | /** 3 | * Convert properties on an object into Solid signal-backed properties. 4 | * 5 | * There are two ways to use this: 6 | * 7 | * 1. Define which properties to convert to signal-backed properties by 8 | * providing property names as trailing arguments. Properties that are 9 | * function-valued (methods) are included as values of the signal properties. 10 | * 2. If no property names are provided, all non-function-valued properties on 11 | * the object will be automatically converted to signal-backed properties. 12 | * 13 | * If any property is already memoified with `memoify()`, or already signalified 14 | * with `signalify()`, it will be skipped. 15 | * 16 | * Example with a class: 17 | * 18 | * ```js 19 | * import {signalify} from 'classy-solid' 20 | * import {createEffect} from 'solid-js' 21 | * 22 | * class Counter { 23 | * count = 0 24 | * 25 | * constructor() { 26 | * signalify(this, 'count') 27 | * setInterval(() => this.count++, 1000) 28 | * } 29 | * } 30 | * 31 | * const counter = new Counter 32 | * 33 | * createEffect(() => { 34 | * console.log('count:', counter.count) 35 | * }) 36 | * ``` 37 | * 38 | * Example with a plain object: 39 | * 40 | * ```js 41 | * import {signalify} from 'classy-solid' 42 | * import {createEffect} from 'solid-js' 43 | * 44 | * const counter = { 45 | * count: 0 46 | * } 47 | * 48 | * signalify(counter, 'count') 49 | * setInterval(() => counter.count++, 1000) 50 | * 51 | * createEffect(() => { 52 | * console.log('count:', counter.count) 53 | * }) 54 | * ``` 55 | */ 56 | export declare function signalify(obj: T): T; 57 | export declare function signalify(obj: T, ...props: (keyof T)[]): T; 58 | /** This overload is for initial value support for downstream use cases. */ 59 | export declare function signalify(obj: T, ...props: [key: keyof T, initialValue: unknown][]): T; 60 | export declare function isPropSetAtLeastOnce__(instance: object, prop: string | symbol): boolean; 61 | export declare function trackPropSetAtLeastOnce__(instance: object, prop: string | symbol): void; 62 | export declare function createSignalAccessor__(obj: T, prop: Exclude, initialVal: unknown, skipFunctionProperties?: boolean): void; 63 | export declare function getSignal__(obj: object, storage: WeakMap>, initialVal: unknown): SignalFunction; 64 | //# sourceMappingURL=signalify.d.ts.map -------------------------------------------------------------------------------- /logo/Favicon_Classy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /dist/decorators/untracked.js: -------------------------------------------------------------------------------- 1 | import { getListener, untrack } from 'solid-js'; 2 | import './metadata-shim.js'; 3 | 4 | /** 5 | * A decorator that makes a class's contructor untracked. 6 | * 7 | * Sometimes, not typically, you may want to ensure that when a class is 8 | * instantiated, any signal reads that happen during the constructor do not 9 | * track those reads. 10 | * 11 | * Normally you do not need to read signals during construction, but if you do, 12 | * you should use `@untracked` to avoid accidentally creating dependencies on 13 | * those signals for any effects that instantiate the class (therefore avoiding 14 | * infinite loops). 15 | * 16 | * Example: 17 | * 18 | * ```ts 19 | * import {untracked, signal} from "classy-solid"; 20 | * import {createEffect} from "solid-js"; 21 | * 22 | * ⁣@untracked 23 | * class Example { 24 | * ⁣@signal count = 0; 25 | * 26 | * constructor() { 27 | * this.count = this.count + 1; // does not track .count signal read in any outer effect. 28 | * } 29 | * } 30 | * 31 | * createEffect(() => { 32 | * // This does not track .count, so this effect will not re-run when .count changes. 33 | * // If this did track .count, an infinite loop would happen. 34 | * const example = new Example(); 35 | * 36 | * createEffect(() => { 37 | * // This inner effect tracks .count, so it will re-run (independent of the 38 | * // outer effect) when .count changes. 39 | * console.log(example.count); 40 | * }); 41 | * }); 42 | * ``` 43 | * 44 | * This can also be called manually without decorators: 45 | * 46 | * ```ts 47 | * import {untracked} from "classy-solid"; 48 | * 49 | * const Example = untracked( 50 | * class { 51 | * count = 0; 52 | * 53 | * constructor() { 54 | * this.count = this.count + 1; // does not track .count signal read in any outer effect. 55 | * } 56 | * } 57 | * ) 58 | * 59 | * // ...same usage as above... 60 | * ``` 61 | */ 62 | export function untracked(value, context) { 63 | // context may be undefined when unsing untracked() without decorators 64 | if (typeof value !== 'function' || context && context.kind !== 'class') throw new TypeError('The @untracked decorator is only for use on classes.'); 65 | const Class = value; 66 | class ReactiveDecorator extends Class { 67 | constructor(...args) { 68 | let instance; 69 | 70 | // Ensure that if we're in an effect that `new`ing a class does not 71 | // track signal reads, otherwise we'll get into an infinite loop. If 72 | // someone want to trigger an effect based on properties of the 73 | // `new`ed instance, they can explicitly read the properties 74 | // themselves in the effect, making their intent clear. 75 | if (getListener()) untrack(() => instance = Reflect.construct(Class, args, new.target)); // super() 76 | else super(...args), instance = this; 77 | return instance; 78 | } 79 | } 80 | return ReactiveDecorator; 81 | } 82 | //# sourceMappingURL=untracked.js.map -------------------------------------------------------------------------------- /dist/signals/createSignalFunction.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createSignalFunction.js","names":["createSignal","createSignalFunction","value","options","get","set","arguments","length"],"sources":["../../src/signals/createSignalFunction.ts"],"sourcesContent":["// FIXME Solid 1.7.9+ requires a TypeScript update, so classy-solid code is made\n// un-typesafe until we update.\n\n// import {createSignal} from './createDeferredEffect.js'\nimport {createSignal} from 'solid-js'\nimport type {Setter} from 'solid-js'\nimport type {SignalOptions} from 'solid-js/types/reactive/signal'\n\n/**\n * A single function that with no args passed reads a signal, otherwise sets a\n * signal just like a Setter does.\n */\nexport type SignalFunction = {(): T} & Setter // FIXME broke in 1.7.9\n\n/**\n * Create a Solid signal wrapped as a single function that gets the value when\n * no arguments are passed in, and sets the value when an argument is passed in.\n * Good for alternative usage patterns, such as when read/write segregation is\n * not needed.\n *\n * ```js\n * let count = createSignalFunction(0) // create it with default value\n * count(1) // set the value\n * count(count() + 1) // increment\n * let currentValue = count() // read the current value\n * ```\n *\n * This is more convenient for class properties than using `createSignal`. With `createSignal`:\n *\n * ```js\n * class Foo {\n * count = createSignal(0)\n *\n * increment() {\n * // difficult to read\n * this.count[1](this.count[0]() + 1)\n *\n * // also:\n * this.count[1](c => c + 1)\n * }\n * }\n * ```\n *\n * With `createSignalFunction`:\n *\n * ```js\n * class Foo {\n * count = createSignalFunction(0)\n *\n * increment() {\n * // Easier to read\n * this.count(this.count() + 1)\n *\n * // also:\n * this.count(c => c + 1)\n * }\n * }\n * ```\n *\n * See also `createSignalObject` for another pattern.\n */\nexport function createSignalFunction(): SignalFunction\nexport function createSignalFunction(value: T, options?: SignalOptions): SignalFunction\nexport function createSignalFunction(value?: T, options?: SignalOptions): SignalFunction {\n\tconst [get, set] = createSignal(value as T, options)\n\n\treturn function (value) {\n\t\tif (arguments.length === 0) return get()\n\t\treturn set(\n\t\t\t// @ts-ignore FIXME its ok, value is defined (even if `undefined`)\n\t\t\tvalue,\n\t\t)\n\t} as SignalFunction\n}\n"],"mappings":"AAAA;AACA;;AAEA;AACA,SAAQA,YAAY,QAAO,UAAU;;AAIrC;AACA;AACA;AACA;AACoD;;AAEpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA,OAAO,SAASC,oBAAoBA,CAAIC,KAAS,EAAEC,OAA0B,EAAqB;EACjG,MAAM,CAACC,GAAG,EAAEC,GAAG,CAAC,GAAGL,YAAY,CAAIE,KAAK,EAAOC,OAAO,CAAC;EAEvD,OAAO,UAAUD,KAAK,EAAE;IACvB,IAAII,SAAS,CAACC,MAAM,KAAK,CAAC,EAAE,OAAOH,GAAG,CAAC,CAAC;IACxC,OAAOC,GAAG;IACT;IACAH,KACD,CAAC;EACF,CAAC;AACF","ignoreList":[]} -------------------------------------------------------------------------------- /src/decorators/untracked.ts: -------------------------------------------------------------------------------- 1 | import type {AnyConstructor} from 'lowclass/dist/Constructor.js' 2 | import {getListener, untrack} from 'solid-js' 3 | import './metadata-shim.js' 4 | 5 | /** 6 | * A decorator that makes a class's contructor untracked. 7 | * 8 | * Sometimes, not typically, you may want to ensure that when a class is 9 | * instantiated, any signal reads that happen during the constructor do not 10 | * track those reads. 11 | * 12 | * Normally you do not need to read signals during construction, but if you do, 13 | * you should use `@untracked` to avoid accidentally creating dependencies on 14 | * those signals for any effects that instantiate the class (therefore avoiding 15 | * infinite loops). 16 | * 17 | * Example: 18 | * 19 | * ```ts 20 | * import {untracked, signal} from "classy-solid"; 21 | * import {createEffect} from "solid-js"; 22 | * 23 | * ⁣@untracked 24 | * class Example { 25 | * ⁣@signal count = 0; 26 | * 27 | * constructor() { 28 | * this.count = this.count + 1; // does not track .count signal read in any outer effect. 29 | * } 30 | * } 31 | * 32 | * createEffect(() => { 33 | * // This does not track .count, so this effect will not re-run when .count changes. 34 | * // If this did track .count, an infinite loop would happen. 35 | * const example = new Example(); 36 | * 37 | * createEffect(() => { 38 | * // This inner effect tracks .count, so it will re-run (independent of the 39 | * // outer effect) when .count changes. 40 | * console.log(example.count); 41 | * }); 42 | * }); 43 | * ``` 44 | * 45 | * This can also be called manually without decorators: 46 | * 47 | * ```ts 48 | * import {untracked} from "classy-solid"; 49 | * 50 | * const Example = untracked( 51 | * class { 52 | * count = 0; 53 | * 54 | * constructor() { 55 | * this.count = this.count + 1; // does not track .count signal read in any outer effect. 56 | * } 57 | * } 58 | * ) 59 | * 60 | * // ...same usage as above... 61 | * ``` 62 | */ 63 | export function untracked(value: AnyConstructor, context: ClassDecoratorContext | undefined): any { 64 | // context may be undefined when unsing untracked() without decorators 65 | if (typeof value !== 'function' || (context && context.kind !== 'class')) 66 | throw new TypeError('The @untracked decorator is only for use on classes.') 67 | 68 | const Class = value 69 | 70 | class ReactiveDecorator extends Class { 71 | constructor(...args: any[]) { 72 | let instance!: ReactiveDecorator 73 | 74 | // Ensure that if we're in an effect that `new`ing a class does not 75 | // track signal reads, otherwise we'll get into an infinite loop. If 76 | // someone want to trigger an effect based on properties of the 77 | // `new`ed instance, they can explicitly read the properties 78 | // themselves in the effect, making their intent clear. 79 | if (getListener()) untrack(() => (instance = Reflect.construct(Class, args, new.target))) // super() 80 | else super(...args), (instance = this) 81 | 82 | return instance 83 | } 84 | } 85 | 86 | return ReactiveDecorator 87 | } 88 | -------------------------------------------------------------------------------- /dist/effects/createDeferredEffect.js: -------------------------------------------------------------------------------- 1 | // TODO switch to non-dep-tracking non-queue-modifying deferred signals, because those do not break with regular effects. 2 | 3 | import { createSignal as _createSignal, createEffect, onCleanup, getOwner, runWithOwner } from 'solid-js'; 4 | const effectQueue = new Set(); 5 | let runningEffects = false; 6 | 7 | // map of effects to dependencies 8 | const effectDeps = new Map(); 9 | let currentEffect = () => {}; 10 | 11 | // Override createSignal in order to implement custom tracking of effect 12 | // dependencies, so that when signals change, we are aware which dependenct 13 | // effects need to be moved to the end of the effect queue while running 14 | // deferred effects in a microtask. 15 | export let createSignal = (value, options) => { 16 | let [_get, _set] = _createSignal(value, options); 17 | const get = () => { 18 | if (!runningEffects) return _get(); 19 | let deps = effectDeps.get(currentEffect); 20 | if (!deps) effectDeps.set(currentEffect, deps = new Set()); 21 | deps.add(_set); 22 | return _get(); 23 | }; 24 | const set = v => { 25 | if (!runningEffects) return _set(v); 26 | 27 | // This is inefficient, for proof of concept, unable to use Solid 28 | // internals on the outside. 29 | for (const [fn, deps] of effectDeps) { 30 | for (const dep of deps) { 31 | if (dep === _set) { 32 | // move to the end 33 | effectQueue.delete(fn); 34 | effectQueue.add(fn); 35 | } 36 | } 37 | } 38 | return _set(v); 39 | }; 40 | return [get, set]; 41 | }; 42 | let effectTaskIsScheduled = false; 43 | 44 | // TODO Option so the first run is deferred instead of immediate? This already 45 | // happens outside of a root. 46 | export const createDeferredEffect = (fn, value, options) => { 47 | let initial = true; 48 | createEffect(prev => { 49 | if (initial) { 50 | initial = false; 51 | currentEffect = fn; 52 | effectDeps.get(fn)?.clear(); // clear to track deps, or else it won't track new deps based on code branching 53 | fn(prev); 54 | return; 55 | } 56 | effectQueue.add(fn); // add, or move to the end, of the queue. TODO This is probably redundant now, but I haven't tested yet. 57 | 58 | // If we're currently running the queue, return because fn will run 59 | // again at the end of the queue iteration due to our overriden 60 | // createSignal moving it to the end. 61 | if (runningEffects) return; 62 | if (effectTaskIsScheduled) return; 63 | effectTaskIsScheduled = true; 64 | const owner = getOwner(); 65 | queueMicrotask(() => { 66 | if (owner) runWithOwner(owner, runEffects);else runEffects(); 67 | }); 68 | }, value, options); 69 | getOwner() && onCleanup(() => { 70 | effectDeps.delete(fn); 71 | effectQueue.delete(fn); 72 | }); 73 | }; 74 | function runEffects() { 75 | runningEffects = true; 76 | for (const fn of effectQueue) { 77 | effectQueue.delete(fn); // TODO This is probably redundant now, but I haven't tested yet. 78 | createDeferredEffect(fn); 79 | } 80 | runningEffects = false; 81 | effectTaskIsScheduled = false; 82 | } 83 | //# sourceMappingURL=createDeferredEffect.js.map -------------------------------------------------------------------------------- /dist/index.test.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | function expect(...args: any[]): any; 3 | } 4 | export declare function testButterflyProps(b: { 5 | colors: number; 6 | wingSize: number; 7 | }, initialColors?: number): void; 8 | declare const MyElement_base: { 9 | new (...a: any[]): { 10 | "__#1@#effectFunctions": (() => void)[]; 11 | "__#1@#started": boolean; 12 | createEffect(fn: () => void): void; 13 | "__#1@#isRestarting": boolean; 14 | startEffects(): void; 15 | stopEffects(): void; 16 | clearEffects(): void; 17 | "__#1@#owner": import("solid-js").Owner | null; 18 | "__#1@#dispose": (() => void) | null; 19 | "__#1@#createEffect"(fn: () => void): void; 20 | }; 21 | } & { 22 | new (): HTMLElement; 23 | prototype: HTMLElement; 24 | }; 25 | export declare class MyElement extends MyElement_base { 26 | a: number; 27 | b: number; 28 | runs: number; 29 | result: number; 30 | connectedCallback(): void; 31 | disconnectedCallback(): void; 32 | } 33 | declare const MyElement2_base: { 34 | new (...a: any[]): { 35 | "__#1@#effectFunctions": (() => void)[]; 36 | "__#1@#started": boolean; 37 | createEffect(fn: () => void): void; 38 | "__#1@#isRestarting": boolean; 39 | startEffects(): void; 40 | stopEffects(): void; 41 | clearEffects(): void; 42 | "__#1@#owner": import("solid-js").Owner | null; 43 | "__#1@#dispose": (() => void) | null; 44 | "__#1@#createEffect"(fn: () => void): void; 45 | }; 46 | } & { 47 | new (): HTMLElement; 48 | prototype: HTMLElement; 49 | }; 50 | export declare class MyElement2 extends MyElement2_base { 51 | a: number; 52 | b: number; 53 | runs: number; 54 | result: number; 55 | constructor(); 56 | connectedCallback(): void; 57 | disconnectedCallback(): void; 58 | } 59 | declare const MyElement3_base: { 60 | new (...a: any[]): { 61 | "__#1@#effectFunctions": (() => void)[]; 62 | "__#1@#started": boolean; 63 | createEffect(fn: () => void): void; 64 | "__#1@#isRestarting": boolean; 65 | startEffects(): void; 66 | stopEffects(): void; 67 | clearEffects(): void; 68 | "__#1@#owner": import("solid-js").Owner | null; 69 | "__#1@#dispose": (() => void) | null; 70 | "__#1@#createEffect"(fn: () => void): void; 71 | }; 72 | } & { 73 | new (): HTMLElement; 74 | prototype: HTMLElement; 75 | }; 76 | export declare class MyElement3 extends MyElement3_base { 77 | runs: number; 78 | result: number; 79 | a: number; 80 | b: number; 81 | get sum(): number; 82 | log(): void; 83 | connectedCallback(): void; 84 | disconnectedCallback(): void; 85 | } 86 | export declare class MyElement4 extends HTMLElement { 87 | runs: number; 88 | result: number; 89 | a: number; 90 | b: number; 91 | get sum(): number; 92 | log(): void; 93 | connectedCallback(): void; 94 | disconnectedCallback(): void; 95 | } 96 | export declare function testElementEffects(el: Element & { 97 | a: number; 98 | b: number; 99 | result: number; 100 | runs: number; 101 | }): void; 102 | export {}; 103 | //# sourceMappingURL=index.test.d.ts.map -------------------------------------------------------------------------------- /src/decorators/untracked.test.ts: -------------------------------------------------------------------------------- 1 | import {createEffect} from 'solid-js' 2 | import {signal} from './signal.js' 3 | import {untracked} from './untracked.js' 4 | import {memo} from './memo.js' 5 | import {reactive} from './reactive.js' 6 | 7 | describe('Reactivity Tracking in Constructors', () => { 8 | it('automatically does not track reactivity in constructors when using @untracked', () => { 9 | @untracked 10 | class Foo { 11 | @signal amount = 3 12 | } 13 | 14 | @untracked 15 | class Bar extends Foo { 16 | @signal double = 0 17 | 18 | constructor() { 19 | super() 20 | this.double = this.amount * 2 // this read of .amount should not be tracked 21 | } 22 | } 23 | 24 | let b: Bar 25 | let count = 0 26 | 27 | function noLoop() { 28 | createEffect(() => { 29 | b = new Bar() // this should not track 30 | count++ 31 | }) 32 | } 33 | 34 | expect(noLoop).not.toThrow() 35 | expect(count).toBe(1) 36 | 37 | const b2 = b! 38 | 39 | b!.amount = 4 // hence this should not trigger 40 | 41 | // If the effect ran only once initially, not when setting b.colors, 42 | // then both variables should reference the same instance 43 | expect(count).toBe(1) 44 | expect(b!).toBe(b2) 45 | }) 46 | 47 | // deprecated 48 | it('automatically does not track reactivity in constructors when using @reactive', () => { 49 | @reactive 50 | class Foo { 51 | @signal amount = 3 52 | } 53 | 54 | @reactive 55 | class Bar extends Foo { 56 | @signal double = 0 57 | 58 | constructor() { 59 | super() 60 | this.double = this.amount * 2 // this read of .amount should not be tracked 61 | } 62 | } 63 | 64 | let b: Bar 65 | let count = 0 66 | 67 | function noLoop() { 68 | createEffect(() => { 69 | b = new Bar() // this should not track 70 | count++ 71 | }) 72 | } 73 | 74 | expect(noLoop).not.toThrow() 75 | expect(count).toBe(1) 76 | 77 | const b2 = b! 78 | 79 | b!.amount = 4 // hence this should not trigger 80 | 81 | // If the effect ran only once initially, not when setting b.colors, 82 | // then both variables should reference the same instance 83 | expect(count).toBe(1) 84 | expect(b!).toBe(b2) 85 | }) 86 | 87 | it('automatically does not track reactivity in constructors when using @memo', () => { 88 | class Foo { 89 | @signal amount = (() => { 90 | debugger 91 | return 3 92 | })() 93 | 94 | // @signal accessor yo = 123 95 | 96 | // @signal get bar() { 97 | // return this 98 | // } 99 | // @signal set bar(v) { 100 | // // do nothing 101 | // } 102 | } 103 | 104 | class Bar extends Foo { 105 | @memo get double() { 106 | return this.amount * 2 107 | } 108 | } 109 | 110 | let b: Bar 111 | let count = 0 112 | 113 | function noLoop() { 114 | createEffect(() => { 115 | b = new Bar() // this should not track 116 | count++ 117 | }) 118 | } 119 | 120 | expect(noLoop).not.toThrow() 121 | expect(count).toBe(1) 122 | 123 | const b2 = b! 124 | 125 | b!.amount = 4 // hence this should not trigger 126 | 127 | // If the effect ran only once initially, not when setting b.colors, 128 | // then both variables should reference the same instance 129 | expect(count).toBe(1) 130 | expect(b!).toBe(b2) 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /dist/decorators/effect.d.ts: -------------------------------------------------------------------------------- 1 | import './metadata-shim.js'; 2 | /** 3 | * Decorator for making Solid.js effects out of methods or function-valued 4 | * properties. This is more convenient than calling `this.createEffect()` in the 5 | * constructor or a class method, reducing boilerplate. Pair this with `@signal` 6 | * and `@memo` to make reactive classes with less code. 7 | * 8 | * The `@effect` decorator can be used on methods or auto accessors. Methods are 9 | * the recommended usage. 10 | * 11 | * When used on auto accessors, the auto accessor value must be a function. Note 12 | * that currently the auto accessor function value cannot be changed (if you 13 | * change it, the new function will not be used). 14 | * 15 | * Example: 16 | * 17 | * ```ts 18 | * import { effect, signal, stopEffects } from 'classy-solid' 19 | * import { createSignal } from 'solid-js' 20 | * 21 | * const [a, setA] = createSignal(1) 22 | * 23 | * class Funkalicious { 24 | * @signal b = 2 25 | * 26 | * @effect logSum() { 27 | * console.log('Sum:', a() + this.b) 28 | * } 29 | * 30 | * // Not recommended, but supported (more concise for simple effects): 31 | * @effect accessor logA = () => console.log('a:', a()) 32 | * } 33 | * 34 | * const fun = new Funkalicious() // logs "Sum: 3" 35 | * 36 | * setA(5) // logs "Sum: 7", "a: 5" 37 | * fun.b = 10 // logs "Sum: 15" 38 | * 39 | * // Later, clean up when done... 40 | * stopEffects(fun) 41 | * ``` 42 | * 43 | * When extending from Effectful() or Effects, the `stopEffects` method can 44 | * be used instead of the standalone `stopEffects()` function: 45 | * 46 | * ```ts 47 | * import { effect, signal, Effects } from 'classy-solid' 48 | * import { createSignal } from 'solid-js' 49 | * 50 | * const [a, setA] = createSignal(1) 51 | * 52 | * class Funkalicious extends Effects { 53 | * @signal b = 2 54 | * 55 | * @effect logSum() { 56 | * console.log('Sum:', a() + this.b) 57 | * } 58 | * } 59 | * 60 | * const fun = new Funkalicious() // logs "Sum: 3" 61 | * 62 | * setA(5) // logs "Sum: 7" 63 | * fun.b = 10 // logs "Sum: 15" 64 | * 65 | * // Later, clean up when done... 66 | * fun.stopEffects() 67 | * ``` 68 | */ 69 | export declare function effect(value: Function | ClassAccessorDecoratorTarget void>, context: ClassMethodDecoratorContext | ClassAccessorDecoratorContext): void; 70 | /** 71 | * Starts all Solid.js effects created by the `@effect` decorator on the given 72 | * object. This can be used to restart effects that were previously stopped with 73 | * `stopEffects()`. 74 | * 75 | * Effects are created and started automatically, so this only needs to be 76 | * called if you have previously stopped the effects and want to start them 77 | * again. 78 | */ 79 | export declare function startEffects(obj: object): void; 80 | /** 81 | * Stops all Solid.js effects created by the `@effect` decorator on the given 82 | * object. Use this once you are done with the instance and need to clean up. 83 | * 84 | * This does not needed to be used if the object is created in a reactive 85 | * context (such as inside a Solid.js component, or a nested effect), as those 86 | * effects will be cleaned up automatically when the owner context is cleaned 87 | * up. 88 | * 89 | * Effects that have been stopped can later be restarted by calling 90 | * `startEffects(obj)`. 91 | */ 92 | export declare function stopEffects(obj: object): void; 93 | //# sourceMappingURL=effect.d.ts.map -------------------------------------------------------------------------------- /dist/example.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"example.js","names":["signal","effect","createEffect","Foo","_init_foo","_init_extra_foo","_initProto","_applyDecs","e","constructor","foo","lorem","v","Bar","_init_bar","_init_extra_bar","_initProto2","args","baz","bar","logFoo","console","log","logLorem","logBar","logBaz","b","setInterval"],"sources":["../src/example.ts"],"sourcesContent":["import {signal, effect} from './index.js'\nimport {createEffect} from 'solid-js'\n\nclass Foo {\n\t@signal foo = 123\n\n\t@signal get lorem() {\n\t\treturn 123\n\t}\n\t@signal set lorem(v) {\n\t\tv\n\t}\n}\n\nclass Bar extends Foo {\n\t// This causes a TDZ error if it comes after bar. See \"TDZ\" example in signal.test.ts.\n\t// Spec ordering issue: https://github.com/tc39/proposal-decorators/issues/571\n\t#baz = 789\n\n\t@signal bar = 456\n\n\t// This would cause a TDZ error.\n\t// #baz = 789\n\n\t@signal get baz() {\n\t\treturn this.#baz\n\t}\n\t@signal set baz(v) {\n\t\tthis.#baz = v\n\t}\n\n\t@effect logFoo() {\n\t\tconsole.log('this.foo:', this.foo)\n\t}\n\n\t@effect logLorem() {\n\t\tconsole.log('this.lorem:', this.lorem)\n\t}\n\n\t@effect logBar() {\n\t\tconsole.log('this.bar:', this.bar)\n\t}\n\n\t@effect logBaz() {\n\t\tconsole.log('this.baz:', this.baz)\n\t}\n}\n\nexport {Foo}\n\nconsole.log('---------')\nconst b = new Bar()\n\ncreateEffect(() => {\n\tconsole.log('b.foo:', b.foo)\n})\n\ncreateEffect(() => {\n\tconsole.log('b.lorem:', b.lorem)\n})\n\ncreateEffect(() => {\n\tconsole.log('b.bar:', b.bar)\n})\n\ncreateEffect(() => {\n\tconsole.log('b.baz:', b.baz)\n})\n\nsetInterval(() => {\n\tconsole.log('---')\n\tb.foo++\n\tb.bar++\n\tb.baz++\n\tb.lorem++\n}, 1000)\n"],"mappings":";;;;;;AAAA,SAAQA,MAAM,EAAEC,MAAM,QAAO,YAAY;AACzC,SAAQC,YAAY,QAAO,UAAU;AAErC,MAAMC,GAAG,CAAC;EAAA;IAAA,CAAAC,SAAA,EAAAC,eAAA,EAAAC,UAAA,IAAAC,UAAA,aACRP,MAAM,cAENA,MAAM,gBAGNA,MAAM,gBAAAQ,CAAA;EAAA;EAAAC,YAAA;IAAAJ,eAAA;EAAA;EALCK,GAAG,IAAAJ,UAAA,QAAAF,SAAA,OAAG,GAAG;EAEjB,IAAYO,KAAKA,CAAA,EAAG;IACnB,OAAO,GAAG;EACX;EACA,IAAYA,KAAKA,CAACC,CAAC,EAAE;IACpBA,CAAC;EACF;AACD;AAEA,MAAMC,GAAG,SAASV,GAAG,CAAC;EAAA;IAAA,CAAAW,SAAA,EAAAC,eAAA,EAAAC,WAAA,IAAAT,UAAA,aAKpBP,MAAM,cAKNA,MAAM,cAGNA,MAAM,cAINC,MAAM,iBAINA,MAAM,mBAINA,MAAM,iBAINA,MAAM,4BA7BUE,GAAG,EAAAK,CAAA;EAAA;EAAAC,YAAA,GAAAQ,IAAA;IAAA,SAAAA,IAAA;IAAAF,eAAA;EAAA;EACpB;EACA;EACA,CAACG,GAAG,IAAAF,WAAA,QAAG,GAAG;EAEFG,GAAG,GAAAL,SAAA,OAAG,GAAG;;EAEjB;EACA;;EAEA,IAAYI,GAAGA,CAAA,EAAG;IACjB,OAAO,IAAI,CAAC,CAACA,GAAG;EACjB;EACA,IAAYA,GAAGA,CAACN,CAAC,EAAE;IAClB,IAAI,CAAC,CAACM,GAAG,GAAGN,CAAC;EACd;EAEQQ,MAAMA,CAAA,EAAG;IAChBC,OAAO,CAACC,GAAG,CAAC,WAAW,EAAE,IAAI,CAACZ,GAAG,CAAC;EACnC;EAEQa,QAAQA,CAAA,EAAG;IAClBF,OAAO,CAACC,GAAG,CAAC,aAAa,EAAE,IAAI,CAACX,KAAK,CAAC;EACvC;EAEQa,MAAMA,CAAA,EAAG;IAChBH,OAAO,CAACC,GAAG,CAAC,WAAW,EAAE,IAAI,CAACH,GAAG,CAAC;EACnC;EAEQM,MAAMA,CAAA,EAAG;IAChBJ,OAAO,CAACC,GAAG,CAAC,WAAW,EAAE,IAAI,CAACJ,GAAG,CAAC;EACnC;AACD;AAEA,SAAQf,GAAG;AAEXkB,OAAO,CAACC,GAAG,CAAC,WAAW,CAAC;AACxB,MAAMI,CAAC,GAAG,IAAIb,GAAG,CAAC,CAAC;AAEnBX,YAAY,CAAC,MAAM;EAClBmB,OAAO,CAACC,GAAG,CAAC,QAAQ,EAAEI,CAAC,CAAChB,GAAG,CAAC;AAC7B,CAAC,CAAC;AAEFR,YAAY,CAAC,MAAM;EAClBmB,OAAO,CAACC,GAAG,CAAC,UAAU,EAAEI,CAAC,CAACf,KAAK,CAAC;AACjC,CAAC,CAAC;AAEFT,YAAY,CAAC,MAAM;EAClBmB,OAAO,CAACC,GAAG,CAAC,QAAQ,EAAEI,CAAC,CAACP,GAAG,CAAC;AAC7B,CAAC,CAAC;AAEFjB,YAAY,CAAC,MAAM;EAClBmB,OAAO,CAACC,GAAG,CAAC,QAAQ,EAAEI,CAAC,CAACR,GAAG,CAAC;AAC7B,CAAC,CAAC;AAEFS,WAAW,CAAC,MAAM;EACjBN,OAAO,CAACC,GAAG,CAAC,KAAK,CAAC;EAClBI,CAAC,CAAChB,GAAG,EAAE;EACPgB,CAAC,CAACP,GAAG,EAAE;EACPO,CAAC,CAACR,GAAG,EAAE;EACPQ,CAAC,CAACf,KAAK,EAAE;AACV,CAAC,EAAE,IAAI,CAAC","ignoreList":[]} -------------------------------------------------------------------------------- /src/decorators/component.ts: -------------------------------------------------------------------------------- 1 | import {Constructor} from 'lowclass/dist/Constructor.js' 2 | import {onMount, createEffect, onCleanup, type JSX, $TRACK, createMemo} from 'solid-js' 3 | import './metadata-shim.js' 4 | 5 | // https://github.com/ryansolid/dom-expressions/pull/122 6 | 7 | interface PossibleComponent { 8 | onMount?(): void 9 | onCleanup?(): void 10 | template?(props: Record): JSX.Element 11 | } 12 | interface PossiblyReactiveConstructor {} 13 | 14 | /** 15 | * A decorator for using classes as Solid components. 16 | * 17 | * Example: 18 | * 19 | * ```js 20 | * ⁣@component 21 | * class MyComp { 22 | * ⁣@signal last = 'none' 23 | * 24 | * onMount() { 25 | * console.log('mounted') 26 | * } 27 | * 28 | * template(props) { 29 | * // here we use `props` passed in, or the signal on `this` which is also 30 | * // treated as a prop 31 | * return

Hello, my name is {props.first} {this.last}

32 | * } 33 | * } 34 | * 35 | * render(() => ) 36 | * ``` 37 | */ 38 | export function component(Base: T, context?: DecoratorContext): any { 39 | if (typeof Base !== 'function' || (context && context.kind !== 'class')) 40 | throw new Error('The @component decorator should only be used on a class.') 41 | 42 | const Class = Constructor(Base) 43 | 44 | // Solid only undetstands function components, so we create a wrapper 45 | // function that instantiates the class and hooks up lifecycle methods and 46 | // props. 47 | function classComponentWrapper(props: Record): JSX.Element { 48 | const instance = new Class() 49 | 50 | const keys = createMemo( 51 | () => { 52 | props[$TRACK] 53 | return Object.keys(props) 54 | }, 55 | [], 56 | { 57 | equals(prev, next) { 58 | if (prev.length !== next.length) return false 59 | for (let i = 0, l = prev.length; i < l; i += 1) if (prev[i] !== next[i]) return false 60 | return true 61 | }, 62 | }, 63 | ) 64 | 65 | createEffect(() => { 66 | // @ts-expect-error index signature 67 | for (const prop of keys()) createEffect(() => (instance[prop] = props[prop])) 68 | }) 69 | 70 | onMount(() => { 71 | instance.onMount?.() 72 | 73 | createEffect(() => { 74 | const ref = props.ref as ((component: PossibleComponent) => void) | undefined 75 | ref?.(instance) 76 | }) 77 | 78 | onCleanup(() => instance.onCleanup?.()) 79 | }) 80 | 81 | return instance.template?.(props) ?? null 82 | } 83 | 84 | Object.defineProperties(classComponentWrapper, { 85 | name: { 86 | value: Class.name, 87 | configurable: true, 88 | }, 89 | [Symbol.hasInstance]: { 90 | value(obj: unknown) { 91 | return obj instanceof Class 92 | }, 93 | configurable: true, 94 | }, 95 | }) 96 | 97 | return classComponentWrapper 98 | } 99 | 100 | declare module 'solid-js' { 101 | namespace JSX { 102 | // Tells JSX what properties class components should have. 103 | interface ElementClass { 104 | template?(props: Record): JSX.Element 105 | } 106 | 107 | // Tells JSX where to look up prop types on class components. 108 | interface ElementAttributesProperty { 109 | PropTypes: {} 110 | } 111 | } 112 | } 113 | 114 | export type Props = Pick & { 115 | children?: JSX.Element 116 | ref?: (component: T) => void 117 | } 118 | -------------------------------------------------------------------------------- /src/effects/createDeferredEffect.ts: -------------------------------------------------------------------------------- 1 | // TODO switch to non-dep-tracking non-queue-modifying deferred signals, because those do not break with regular effects. 2 | 3 | import {createSignal as _createSignal, createEffect, onCleanup, getOwner, runWithOwner} from 'solid-js' 4 | 5 | import type {EffectFunction} from 'solid-js/types/reactive/signal' 6 | 7 | const effectQueue: Set> = new Set() 8 | let runningEffects = false 9 | 10 | // map of effects to dependencies 11 | const effectDeps = new Map, Set<(v: any) => any>>() 12 | let currentEffect: EffectFunction = () => {} 13 | 14 | // Override createSignal in order to implement custom tracking of effect 15 | // dependencies, so that when signals change, we are aware which dependenct 16 | // effects need to be moved to the end of the effect queue while running 17 | // deferred effects in a microtask. 18 | export let createSignal = ((value, options) => { 19 | let [_get, _set] = _createSignal(value, options) 20 | 21 | const get = (() => { 22 | if (!runningEffects) return _get() 23 | 24 | let deps = effectDeps.get(currentEffect) 25 | if (!deps) effectDeps.set(currentEffect, (deps = new Set())) 26 | deps.add(_set) 27 | 28 | return _get() 29 | }) as typeof _get 30 | 31 | const set = (v => { 32 | if (!runningEffects) return _set(v as any) 33 | 34 | // This is inefficient, for proof of concept, unable to use Solid 35 | // internals on the outside. 36 | for (const [fn, deps] of effectDeps) { 37 | for (const dep of deps) { 38 | if (dep === _set) { 39 | // move to the end 40 | effectQueue.delete(fn) 41 | effectQueue.add(fn) 42 | } 43 | } 44 | } 45 | 46 | return _set(v as any) 47 | }) as typeof _set 48 | 49 | return [get, set] 50 | }) as typeof _createSignal 51 | 52 | let effectTaskIsScheduled = false 53 | 54 | // TODO Option so the first run is deferred instead of immediate? This already 55 | // happens outside of a root. 56 | export const createDeferredEffect = ((fn, value, options) => { 57 | let initial = true 58 | 59 | createEffect( 60 | (prev: any) => { 61 | if (initial) { 62 | initial = false 63 | 64 | currentEffect = fn 65 | effectDeps.get(fn)?.clear() // clear to track deps, or else it won't track new deps based on code branching 66 | fn(prev) 67 | 68 | return 69 | } 70 | 71 | effectQueue.add(fn) // add, or move to the end, of the queue. TODO This is probably redundant now, but I haven't tested yet. 72 | 73 | // If we're currently running the queue, return because fn will run 74 | // again at the end of the queue iteration due to our overriden 75 | // createSignal moving it to the end. 76 | if (runningEffects) return 77 | 78 | if (effectTaskIsScheduled) return 79 | 80 | effectTaskIsScheduled = true 81 | 82 | const owner = getOwner() 83 | 84 | queueMicrotask(() => { 85 | if (owner) runWithOwner(owner, runEffects) 86 | else runEffects() 87 | }) 88 | }, 89 | value, 90 | options, 91 | ) 92 | 93 | getOwner() && 94 | onCleanup(() => { 95 | effectDeps.delete(fn) 96 | effectQueue.delete(fn) 97 | }) 98 | }) as typeof createEffect 99 | 100 | function runEffects() { 101 | runningEffects = true 102 | 103 | for (const fn of effectQueue) { 104 | effectQueue.delete(fn) // TODO This is probably redundant now, but I haven't tested yet. 105 | createDeferredEffect(fn) 106 | } 107 | 108 | runningEffects = false 109 | effectTaskIsScheduled = false 110 | } 111 | -------------------------------------------------------------------------------- /dist/effects/createDeferredEffect.test.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createDeferredEffect.test.js","names":["createRoot","createSignalFunction","createDeferredEffect","describe","it","count","foo","runCount","stop","_stop","expect","toBe","Promise","resolve"],"sources":["../../src/effects/createDeferredEffect.test.ts"],"sourcesContent":["import {createRoot} from 'solid-js'\nimport {createSignalFunction} from '../signals/createSignalFunction.js'\nimport {createDeferredEffect} from './createDeferredEffect.js'\n\ndescribe('classy-solid', () => {\n\tdescribe('createDeferredEffect()', () => {\n\t\tit('works', async () => {\n\t\t\tconst count = createSignalFunction(0)\n\t\t\tconst foo = createSignalFunction(0)\n\n\t\t\tlet runCount = 0\n\n\t\t\tconst stop = (() => {\n\t\t\t\tlet stop!: () => void\n\n\t\t\t\tcreateRoot(_stop => {\n\t\t\t\t\tstop = _stop\n\n\t\t\t\t\t// Runs once initially after the current root context just\n\t\t\t\t\t// like createEffect, then any time it re-runs due to a\n\t\t\t\t\t// change in a dependency, the re-run will be deferred in\n\t\t\t\t\t// the next microtask and will run only once (not once per\n\t\t\t\t\t// signal that changed)\n\t\t\t\t\tcreateDeferredEffect(() => {\n\t\t\t\t\t\tcount()\n\t\t\t\t\t\tfoo()\n\t\t\t\t\t\trunCount++\n\t\t\t\t\t})\n\t\t\t\t})\n\n\t\t\t\treturn stop\n\t\t\t})()\n\n\t\t\t// Queues the effect to run in the next microtask\n\t\t\tcount(1)\n\t\t\tcount(2)\n\t\t\tfoo(3)\n\n\t\t\t// Still 1 because the deferred effect didn't run yet, it will in the next microtask.\n\t\t\texpect(runCount).toBe(1)\n\n\t\t\tawait Promise.resolve()\n\n\t\t\t// It ran only once in the previous microtask (batched), not once per signal write.\n\t\t\texpect(runCount).toBe(2)\n\n\t\t\tcount(3)\n\t\t\tcount(4)\n\t\t\tfoo(5)\n\n\t\t\texpect(runCount).toBe(2)\n\n\t\t\tawait Promise.resolve()\n\n\t\t\texpect(runCount).toBe(3)\n\n\t\t\t// Stops the effect from re-running. It can now be garbage collected.\n\t\t\tstop()\n\n\t\t\tcount(3)\n\t\t\tcount(4)\n\t\t\tfoo(5)\n\n\t\t\texpect(runCount).toBe(3)\n\n\t\t\tawait Promise.resolve()\n\n\t\t\t// Still the same because it was stopped, so it didn't run in the\n\t\t\t// macrotask prior to the await.\n\t\t\texpect(runCount).toBe(3)\n\n\t\t\t// Double check just in case (the wrong implementation would make it\n\t\t\t// skip two microtasks before running).\n\t\t\tawait Promise.resolve()\n\t\t\texpect(runCount).toBe(3)\n\t\t})\n\t})\n})\n"],"mappings":"AAAA,SAAQA,UAAU,QAAO,UAAU;AACnC,SAAQC,oBAAoB,QAAO,oCAAoC;AACvE,SAAQC,oBAAoB,QAAO,2BAA2B;AAE9DC,QAAQ,CAAC,cAAc,EAAE,MAAM;EAC9BA,QAAQ,CAAC,wBAAwB,EAAE,MAAM;IACxCC,EAAE,CAAC,OAAO,EAAE,YAAY;MACvB,MAAMC,KAAK,GAAGJ,oBAAoB,CAAC,CAAC,CAAC;MACrC,MAAMK,GAAG,GAAGL,oBAAoB,CAAC,CAAC,CAAC;MAEnC,IAAIM,QAAQ,GAAG,CAAC;MAEhB,MAAMC,IAAI,GAAG,CAAC,MAAM;QACnB,IAAIA,IAAiB;QAErBR,UAAU,CAACS,KAAK,IAAI;UACnBD,IAAI,GAAGC,KAAK;;UAEZ;UACA;UACA;UACA;UACA;UACAP,oBAAoB,CAAC,MAAM;YAC1BG,KAAK,CAAC,CAAC;YACPC,GAAG,CAAC,CAAC;YACLC,QAAQ,EAAE;UACX,CAAC,CAAC;QACH,CAAC,CAAC;QAEF,OAAOC,IAAI;MACZ,CAAC,EAAE,CAAC;;MAEJ;MACAH,KAAK,CAAC,CAAC,CAAC;MACRA,KAAK,CAAC,CAAC,CAAC;MACRC,GAAG,CAAC,CAAC,CAAC;;MAEN;MACAI,MAAM,CAACH,QAAQ,CAAC,CAACI,IAAI,CAAC,CAAC,CAAC;MAExB,MAAMC,OAAO,CAACC,OAAO,CAAC,CAAC;;MAEvB;MACAH,MAAM,CAACH,QAAQ,CAAC,CAACI,IAAI,CAAC,CAAC,CAAC;MAExBN,KAAK,CAAC,CAAC,CAAC;MACRA,KAAK,CAAC,CAAC,CAAC;MACRC,GAAG,CAAC,CAAC,CAAC;MAENI,MAAM,CAACH,QAAQ,CAAC,CAACI,IAAI,CAAC,CAAC,CAAC;MAExB,MAAMC,OAAO,CAACC,OAAO,CAAC,CAAC;MAEvBH,MAAM,CAACH,QAAQ,CAAC,CAACI,IAAI,CAAC,CAAC,CAAC;;MAExB;MACAH,IAAI,CAAC,CAAC;MAENH,KAAK,CAAC,CAAC,CAAC;MACRA,KAAK,CAAC,CAAC,CAAC;MACRC,GAAG,CAAC,CAAC,CAAC;MAENI,MAAM,CAACH,QAAQ,CAAC,CAACI,IAAI,CAAC,CAAC,CAAC;MAExB,MAAMC,OAAO,CAACC,OAAO,CAAC,CAAC;;MAEvB;MACA;MACAH,MAAM,CAACH,QAAQ,CAAC,CAACI,IAAI,CAAC,CAAC,CAAC;;MAExB;MACA;MACA,MAAMC,OAAO,CAACC,OAAO,CAAC,CAAC;MACvBH,MAAM,CAACH,QAAQ,CAAC,CAACI,IAAI,CAAC,CAAC,CAAC;IACzB,CAAC,CAAC;EACH,CAAC,CAAC;AACH,CAAC,CAAC","ignoreList":[]} -------------------------------------------------------------------------------- /dist/decorators/untracked.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"untracked.js","names":["getListener","untrack","untracked","value","context","kind","TypeError","Class","ReactiveDecorator","constructor","args","instance","Reflect","construct","new","target"],"sources":["../../src/decorators/untracked.ts"],"sourcesContent":["import type {AnyConstructor} from 'lowclass/dist/Constructor.js'\nimport {getListener, untrack} from 'solid-js'\nimport './metadata-shim.js'\n\n/**\n * A decorator that makes a class's contructor untracked.\n *\n * Sometimes, not typically, you may want to ensure that when a class is\n * instantiated, any signal reads that happen during the constructor do not\n * track those reads.\n *\n * Normally you do not need to read signals during construction, but if you do,\n * you should use `@untracked` to avoid accidentally creating dependencies on\n * those signals for any effects that instantiate the class (therefore avoiding\n * infinite loops).\n *\n * Example:\n *\n * ```ts\n * import {untracked, signal} from \"classy-solid\";\n * import {createEffect} from \"solid-js\";\n *\n * ⁣@untracked\n * class Example {\n * ⁣@signal count = 0;\n *\n * constructor() {\n * this.count = this.count + 1; // does not track .count signal read in any outer effect.\n * }\n * }\n *\n * createEffect(() => {\n * // This does not track .count, so this effect will not re-run when .count changes.\n * // If this did track .count, an infinite loop would happen.\n * const example = new Example();\n *\n * createEffect(() => {\n * // This inner effect tracks .count, so it will re-run (independent of the\n * // outer effect) when .count changes.\n * console.log(example.count);\n * });\n * });\n * ```\n *\n * This can also be called manually without decorators:\n *\n * ```ts\n * import {untracked} from \"classy-solid\";\n *\n * const Example = untracked(\n * class {\n * count = 0;\n *\n * constructor() {\n * this.count = this.count + 1; // does not track .count signal read in any outer effect.\n * }\n * }\n * )\n *\n * // ...same usage as above...\n * ```\n */\nexport function untracked(value: AnyConstructor, context: ClassDecoratorContext | undefined): any {\n\t// context may be undefined when unsing untracked() without decorators\n\tif (typeof value !== 'function' || (context && context.kind !== 'class'))\n\t\tthrow new TypeError('The @untracked decorator is only for use on classes.')\n\n\tconst Class = value\n\n\tclass ReactiveDecorator extends Class {\n\t\tconstructor(...args: any[]) {\n\t\t\tlet instance!: ReactiveDecorator\n\n\t\t\t// Ensure that if we're in an effect that `new`ing a class does not\n\t\t\t// track signal reads, otherwise we'll get into an infinite loop. If\n\t\t\t// someone want to trigger an effect based on properties of the\n\t\t\t// `new`ed instance, they can explicitly read the properties\n\t\t\t// themselves in the effect, making their intent clear.\n\t\t\tif (getListener()) untrack(() => (instance = Reflect.construct(Class, args, new.target))) // super()\n\t\t\telse super(...args), (instance = this)\n\n\t\t\treturn instance\n\t\t}\n\t}\n\n\treturn ReactiveDecorator\n}\n"],"mappings":"AACA,SAAQA,WAAW,EAAEC,OAAO,QAAO,UAAU;AAC7C,OAAO,oBAAoB;;AAE3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,SAASA,CAACC,KAAqB,EAAEC,OAA0C,EAAO;EACjG;EACA,IAAI,OAAOD,KAAK,KAAK,UAAU,IAAKC,OAAO,IAAIA,OAAO,CAACC,IAAI,KAAK,OAAQ,EACvE,MAAM,IAAIC,SAAS,CAAC,sDAAsD,CAAC;EAE5E,MAAMC,KAAK,GAAGJ,KAAK;EAEnB,MAAMK,iBAAiB,SAASD,KAAK,CAAC;IACrCE,WAAWA,CAAC,GAAGC,IAAW,EAAE;MAC3B,IAAIC,QAA4B;;MAEhC;MACA;MACA;MACA;MACA;MACA,IAAIX,WAAW,CAAC,CAAC,EAAEC,OAAO,CAAC,MAAOU,QAAQ,GAAGC,OAAO,CAACC,SAAS,CAACN,KAAK,EAAEG,IAAI,EAAEI,GAAG,CAACC,MAAM,CAAE,CAAC,EAAC;MAAA,KACrF,KAAK,CAAC,GAAGL,IAAI,CAAC,EAAGC,QAAQ,GAAG,IAAK;MAEtC,OAAOA,QAAQ;IAChB;EACD;EAEA,OAAOH,iBAAiB;AACzB","ignoreList":[]} -------------------------------------------------------------------------------- /dist/decorators/memo.d.ts: -------------------------------------------------------------------------------- 1 | import './metadata-shim.js'; 2 | /** 3 | * A decorator that make a signal property derived from a memoized computation 4 | * based on other signals. Effects depending on this property will re-run when 5 | * the computed value changes, but not if the computed value stays the same even 6 | * if the dependencies changed. 7 | * 8 | * @example 9 | * ```ts 10 | * import {reactive, signal, memo} from "classy-solid"; 11 | * 12 | * class Example { 13 | * @signal a = 1 14 | * @signal b = 2 15 | * 16 | * // @memo can be used on a getter, accessor, or method, with readonly and 17 | * // writable variants: 18 | * 19 | * // Readonly memo via getter only 20 | * @memo get sum1() { 21 | * return this.a + this.b 22 | * } 23 | * 24 | * // Writable memo via getter with setter 25 | * @memo get sum2() { 26 | * return this.a + this.b 27 | * } 28 | * @memo set sum2(value: number) { 29 | * // use an empty setter to enable writable (logic in here will be ignored if any) 30 | * } 31 | * 32 | * // Readonly memo via auto accessor (requires arrow function, not writable because no parameter (arity = 0)) 33 | * @memo accessor sum3 = () => this.a + this.b 34 | * 35 | * // Writable memo via auto accessor (requires arrow function with parameter, arity > 0) 36 | * // Ignore the parameter, it only enables writable memo 37 | * @memo accessor sum4 = (v?: number) => this.a + this.b 38 | * 39 | * // Readonly memo via method 40 | * @memo sum5() { 41 | * return this.a + this.b 42 | * } 43 | * 44 | * // Writable memo via method with parameter (arity > 0) 45 | * @memo sum6(value?: number) { 46 | * // Ignore the parameter, it only enables writable memo 47 | * return this.a + this.b 48 | * } 49 | * 50 | * // The following variants are not supported yet as no runtime or TS support exists yet for the syntax. 51 | * 52 | * // Writable memo via accessor, alternative long-hand syntax (not yet released, no runtime or TS support yet) 53 | * @memo accessor sum6 { get; set } = () => this.a + this.b 54 | * 55 | * // Readonly memo via accessor with only getter (not released yet, no runtime or TS support yet) 56 | * @memo accessor sum7 { get; } = () => this.a + this.b 57 | * 58 | * // Readonly memo via accessor with only getter, alternative syntax (not released yet, no runtime or TS support yet) 59 | * @memo accessor sum8 { 60 | * get() { 61 | * return this.a + this.b 62 | * } 63 | * } 64 | * 65 | * // Readonly memo via accessor with only getter, alternative syntax (not released yet, no runtime or TS support yet) 66 | * @memo accessor sum8 { 67 | * get() { 68 | * return this.a + this.b 69 | * } 70 | * set(_v: number) { 71 | * // empty setter makes it writable (logic in here will be ignored if any) 72 | * } 73 | * } 74 | * } 75 | * 76 | * const ex = new Example() 77 | * 78 | * console.log(ex.sum1, ex.sum2, ex.sum3(), ex.sum4(), ex.sum5(), ex.sum6()) // 3 3 3 3 3 3 79 | * 80 | * createEffect(() => { 81 | * console.log(ex.sum1, ex.sum2, ex.sum3(), ex.sum4(), ex.sum5(), ex.sum6()) 82 | * }); 83 | * 84 | * ex.a = 5 // Logs: 7 7 7 7 7 7 85 | * 86 | * // This won't log anything since the computed memo values don't change (all still 7). 87 | * batch(() => { 88 | * ex.a = 3 89 | * ex.b = 4 90 | * }) 91 | * 92 | * ex.sum2 = 20 // Logs: 20 7 7 7 7 7 (only sum2 is updated) 93 | * 94 | * ex.sum6(15) // Logs: 20 7 7 7 7 15 (only sum6 is updated) 95 | * 96 | * ex.sum1 = 10 // Runtime error: Cannot set readonly property "sum1". 97 | * ``` 98 | */ 99 | export declare function memo(value: ((val?: any) => any) | (() => any) | ((val?: any) => void) | (() => void) | ClassAccessorDecoratorTarget any> | ClassAccessorDecoratorTarget any>, // today's auto-accessors, writable memo 100 | context: ClassGetterDecoratorContext | ClassSetterDecoratorContext | ClassAccessorDecoratorContext | ClassMethodDecoratorContext): void; 101 | //# sourceMappingURL=memo.d.ts.map -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import {createComputed, createRoot} from 'solid-js' 2 | import {Effectful} from './mixins/index.js' 3 | import {effect, memo, signal, startEffects, stopEffects} from './decorators/index.js' 4 | 5 | // TODO move type def to @lume/cli, map @types/jest's `expect` type into the 6 | // global env. 7 | declare global { 8 | function expect(...args: any[]): any 9 | } 10 | 11 | // Test helper shared with other test files. 12 | export function testButterflyProps(b: {colors: number; wingSize: number}, initialColors = 3) { 13 | let count = 0 14 | 15 | createRoot(() => { 16 | createComputed(() => { 17 | b.colors 18 | b.wingSize 19 | count++ 20 | }) 21 | }) 22 | 23 | expect(b.colors).toBe(initialColors, 'initial colors value') 24 | expect(b.wingSize).toBe(2, 'initial wingSize value') 25 | expect(count).toBe(1, 'Should be reactive') 26 | 27 | b.colors++ 28 | 29 | expect(b.colors).toBe(initialColors + 1, 'incremented colors value') 30 | expect(count).toBe(2, 'Should be reactive') 31 | 32 | b.wingSize++ 33 | 34 | expect(b.wingSize).toBe(3, 'incremented wingSize value') 35 | expect(count).toBe(3, 'Should be reactive') 36 | } 37 | 38 | export class MyElement extends Effectful(HTMLElement) { 39 | static { 40 | customElements.define('my-element', this) 41 | } 42 | 43 | @signal a = 1 44 | @signal b = 2 45 | 46 | runs = 0 47 | result = 0 48 | 49 | connectedCallback() { 50 | this.createEffect(() => { 51 | this.runs++ 52 | this.result = this.a + this.b 53 | }) 54 | } 55 | 56 | disconnectedCallback() { 57 | this.clearEffects() 58 | } 59 | } 60 | 61 | export class MyElement2 extends Effectful(HTMLElement) { 62 | static { 63 | customElements.define('my-element2', this) 64 | } 65 | 66 | @signal a = 1 67 | @signal b = 2 68 | 69 | runs = 0 70 | result = 0 71 | 72 | constructor() { 73 | super() 74 | this.createEffect(() => { 75 | this.runs++ 76 | this.result = this.a + this.b 77 | }) 78 | } 79 | 80 | connectedCallback() { 81 | this.startEffects() 82 | } 83 | 84 | disconnectedCallback() { 85 | this.stopEffects() 86 | } 87 | } 88 | 89 | export class MyElement3 extends Effectful(HTMLElement) { 90 | static { 91 | customElements.define('my-element3', this) 92 | } 93 | 94 | runs = 0 95 | result = 0 96 | 97 | @signal a = 1 98 | @signal b = 2 99 | 100 | @memo get sum() { 101 | return this.a + this.b 102 | } 103 | 104 | @effect log() { 105 | this.runs++ 106 | this.result = this.sum 107 | } 108 | 109 | connectedCallback() { 110 | this.startEffects() 111 | } 112 | 113 | disconnectedCallback() { 114 | this.stopEffects() 115 | } 116 | } 117 | export class MyElement4 extends HTMLElement { 118 | static { 119 | customElements.define('my-element4', this) 120 | } 121 | 122 | runs = 0 123 | result = 0 124 | 125 | @signal a = 1 126 | @signal b = 2 127 | 128 | @memo get sum() { 129 | return this.a + this.b 130 | } 131 | 132 | @effect log() { 133 | this.runs++ 134 | this.result = this.sum 135 | } 136 | 137 | connectedCallback() { 138 | startEffects(this) 139 | } 140 | 141 | disconnectedCallback() { 142 | stopEffects(this) 143 | } 144 | } 145 | 146 | export function testElementEffects(el: Element & {a: number; b: number; result: number; runs: number}) { 147 | document.body.append(el) // triggers connectedCallback 148 | 149 | expect(el.result).toBe(1 + 2) 150 | expect(el.runs).toBe(1) 151 | 152 | el.a = 5 153 | expect(el.result).toBe(5 + 2) 154 | expect(el.runs).toBe(2) 155 | 156 | el.b = 10 157 | expect(el.result).toBe(5 + 10) 158 | expect(el.runs).toBe(3) 159 | 160 | // disconnect the element 161 | 162 | document.body.removeChild(el) // triggers disconnectedCallback 163 | 164 | // Further signal changes do not affect result while disconnected 165 | el.a = 20 166 | el.b = 30 167 | expect(el.result).toBe(5 + 10) 168 | expect(el.runs).toBe(3) 169 | 170 | // reconnect the element 171 | document.body.append(el) // triggers connectedCallback 172 | expect(el.result).toBe(20 + 30) 173 | expect(el.runs).toBe(4) 174 | 175 | // further signal changes work again 176 | el.a = 100 177 | expect(el.result).toBe(100 + 30) 178 | expect(el.runs).toBe(5) 179 | el.b = 200 180 | expect(el.result).toBe(100 + 200) 181 | expect(el.runs).toBe(6) 182 | } 183 | -------------------------------------------------------------------------------- /dist/decorators/effect.js: -------------------------------------------------------------------------------- 1 | import { effectifyIfNeeded, finalizeMembersIfLast, getMemberStat, getMembers, effects__ } from '../_state.js'; 2 | import './metadata-shim.js'; 3 | 4 | /** 5 | * Decorator for making Solid.js effects out of methods or function-valued 6 | * properties. This is more convenient than calling `this.createEffect()` in the 7 | * constructor or a class method, reducing boilerplate. Pair this with `@signal` 8 | * and `@memo` to make reactive classes with less code. 9 | * 10 | * The `@effect` decorator can be used on methods or auto accessors. Methods are 11 | * the recommended usage. 12 | * 13 | * When used on auto accessors, the auto accessor value must be a function. Note 14 | * that currently the auto accessor function value cannot be changed (if you 15 | * change it, the new function will not be used). 16 | * 17 | * Example: 18 | * 19 | * ```ts 20 | * import { effect, signal, stopEffects } from 'classy-solid' 21 | * import { createSignal } from 'solid-js' 22 | * 23 | * const [a, setA] = createSignal(1) 24 | * 25 | * class Funkalicious { 26 | * @signal b = 2 27 | * 28 | * @effect logSum() { 29 | * console.log('Sum:', a() + this.b) 30 | * } 31 | * 32 | * // Not recommended, but supported (more concise for simple effects): 33 | * @effect accessor logA = () => console.log('a:', a()) 34 | * } 35 | * 36 | * const fun = new Funkalicious() // logs "Sum: 3" 37 | * 38 | * setA(5) // logs "Sum: 7", "a: 5" 39 | * fun.b = 10 // logs "Sum: 15" 40 | * 41 | * // Later, clean up when done... 42 | * stopEffects(fun) 43 | * ``` 44 | * 45 | * When extending from Effectful() or Effects, the `stopEffects` method can 46 | * be used instead of the standalone `stopEffects()` function: 47 | * 48 | * ```ts 49 | * import { effect, signal, Effects } from 'classy-solid' 50 | * import { createSignal } from 'solid-js' 51 | * 52 | * const [a, setA] = createSignal(1) 53 | * 54 | * class Funkalicious extends Effects { 55 | * @signal b = 2 56 | * 57 | * @effect logSum() { 58 | * console.log('Sum:', a() + this.b) 59 | * } 60 | * } 61 | * 62 | * const fun = new Funkalicious() // logs "Sum: 3" 63 | * 64 | * setA(5) // logs "Sum: 7" 65 | * fun.b = 10 // logs "Sum: 15" 66 | * 67 | * // Later, clean up when done... 68 | * fun.stopEffects() 69 | * ``` 70 | */ 71 | 72 | export function effect(value, context) { 73 | if (context.static) throw new Error('@effect is not supported on static members.'); 74 | const { 75 | kind, 76 | name 77 | } = context; 78 | const metadata = context.metadata; 79 | const signalsAndMemos = getMembers(metadata); 80 | if (!(kind === 'method' || kind === 'accessor')) throw new Error('@effect can only be used on methods or function-valued accessors'); 81 | const stat = kind === 'accessor' ? getMemberStat(name, 'effect-auto-accessor', signalsAndMemos) : getMemberStat(name, 'effect-method', signalsAndMemos); 82 | stat.finalize = function () { 83 | effectifyIfNeeded(this, name, stat); 84 | }; 85 | context.addInitializer(function () { 86 | finalizeMembersIfLast(this, signalsAndMemos); 87 | }); 88 | if (kind === 'method') stat.value = value;else if (kind === 'accessor') stat.value = value.get; 89 | } 90 | 91 | /** 92 | * Starts all Solid.js effects created by the `@effect` decorator on the given 93 | * object. This can be used to restart effects that were previously stopped with 94 | * `stopEffects()`. 95 | * 96 | * Effects are created and started automatically, so this only needs to be 97 | * called if you have previously stopped the effects and want to start them 98 | * again. 99 | */ 100 | export function startEffects(obj) { 101 | let effects = effects__.get(obj); 102 | if (!effects) return; 103 | effects.startEffects(); 104 | } 105 | 106 | /** 107 | * Stops all Solid.js effects created by the `@effect` decorator on the given 108 | * object. Use this once you are done with the instance and need to clean up. 109 | * 110 | * This does not needed to be used if the object is created in a reactive 111 | * context (such as inside a Solid.js component, or a nested effect), as those 112 | * effects will be cleaned up automatically when the owner context is cleaned 113 | * up. 114 | * 115 | * Effects that have been stopped can later be restarted by calling 116 | * `startEffects(obj)`. 117 | */ 118 | export function stopEffects(obj) { 119 | const effects = effects__.get(obj); 120 | if (!effects) return; 121 | effects.stopEffects(); 122 | } 123 | //# sourceMappingURL=effect.js.map -------------------------------------------------------------------------------- /dist/signals/createSignalFunction.test.js: -------------------------------------------------------------------------------- 1 | import { createSignalFunction } from './createSignalFunction.js'; 2 | describe('classy-solid', () => { 3 | describe('createSignalFunction()', () => { 4 | it('has gettable and settable values via a single overloaded function', () => { 5 | const num = createSignalFunction(0); 6 | 7 | // Set the variable's value by passing a value in. 8 | num(1); 9 | // Read the variable's value by calling it with no args. 10 | expect(num()).toBe(1); 11 | 12 | // increment example: 13 | const setResult = num(num() + 1); 14 | expect(num()).toBe(2); 15 | expect(setResult).toBe(2); 16 | 17 | // Set with a function that accepts the previous value and returns the next value. 18 | num(n => n + 1); 19 | expect(num()).toBe(3); 20 | }); 21 | it('works with function values', () => { 22 | const f1 = () => 123; 23 | const func = createSignalFunction(f1); 24 | expect(func()).toBe(f1); 25 | const f2 = () => 456; 26 | func(() => f2); 27 | expect(func()).toBe(f2); 28 | }); 29 | }); 30 | }); 31 | 32 | ////////////////////////////////////////////////////////////////////////// 33 | // createSignalFunction type tests /////////////////////////////////////// 34 | ////////////////////////////////////////////////////////////////////////// 35 | 36 | { 37 | const num = createSignalFunction(1); 38 | let n1 = num(); 39 | n1; 40 | num(123); 41 | num(n => n1 = n + 1); 42 | num(); 43 | const num3 = createSignalFunction(); 44 | // @ts-expect-error Type 'undefined' is not assignable to type 'number'. ts(2322) 45 | let n3 = num3(); 46 | num3(123); 47 | num3(undefined); // ok, accepts undefined 48 | // @ts-expect-error Object is possibly 'undefined'. ts(2532) (the `n` value) 49 | num3(n => n3 = n + 1); 50 | num3(); // ok, getter 51 | 52 | // @ts-expect-error Argument of type 'boolean' is not assignable to parameter of type 'number'. ts(2345) 53 | const num4 = createSignalFunction(true); 54 | const bool = createSignalFunction(true); 55 | let b1 = bool(); 56 | b1; 57 | bool(false); 58 | bool(b => b1 = !b); 59 | bool(); 60 | const bool2 = createSignalFunction(); 61 | // @ts-expect-error Type 'undefined' is not assignable to type 'number'. ts(2322) 62 | let n4 = bool2(); 63 | bool2(false); 64 | bool2(undefined); // ok, accepts undefined 65 | bool2(n => n4 = !n); // ok because undefined is being converted to boolean 66 | // @ts-expect-error Type 'boolean | undefined' is not assignable to type 'boolean'. ts(2322) 67 | bool2(n => n4 = n); 68 | bool2(); // ok, accepts undefined 69 | 70 | const func = createSignalFunction(() => 1); 71 | // @ts-expect-error 1 is not assignable to function (no overload matches) 72 | func(() => 1); 73 | func(() => () => 1); // ok, set the value to a function 74 | const fn = func(); // ok, returns function value 75 | fn; 76 | const n5 = func()(); 77 | n5; 78 | const func2 = createSignalFunction(() => 1); 79 | // @ts-expect-error number is not assignable to function (no overload matches) 80 | func2(() => 1); 81 | func2(() => () => 1); // ok, set the value to a function 82 | const fn2 = func2(); // ok, returns function value 83 | fn2; 84 | const n6 = func2()(); 85 | n6; 86 | const stringOrFunc1 = createSignalFunction(''); 87 | // @ts-expect-error number not assignable to string | (()=>number) | undefined 88 | stringOrFunc1(() => 1); 89 | const sf1 = stringOrFunc1(() => () => 1); 90 | sf1; 91 | const sf2 = stringOrFunc1('oh yeah'); 92 | sf2; 93 | const sf3 = stringOrFunc1(() => 'oh yeah'); 94 | sf3; 95 | stringOrFunc1(); // ok, getter 96 | // @ts-expect-error cannot set signal to undefined 97 | stringOrFunc1(undefined); 98 | // @ts-expect-error return value might be string 99 | const sf6 = stringOrFunc1(); 100 | sf6; 101 | const sf7 = stringOrFunc1(); 102 | sf7; 103 | const sf8 = stringOrFunc1(); 104 | sf8; 105 | const stringOrFunc2 = createSignalFunction(); 106 | // @ts-expect-error number not assignable to string | (()=>number) | undefined 107 | stringOrFunc2(() => 1); 108 | const sf9 = stringOrFunc2(() => () => 1); 109 | sf9; 110 | const sf10 = stringOrFunc2('oh yeah'); 111 | sf10; 112 | const sf11 = stringOrFunc2(() => 'oh yeah'); 113 | sf11; 114 | // @ts-expect-error 'string | (() => number) | undefined' is not assignable to type 'undefined'. 115 | const sf12 = stringOrFunc2(); 116 | sf12; 117 | const sf13 = stringOrFunc2(undefined); 118 | sf13; 119 | const sf14 = stringOrFunc2(); 120 | sf14; 121 | // @ts-expect-error return value might be undefined 122 | const sf15 = stringOrFunc2(); 123 | sf15; 124 | } 125 | //# sourceMappingURL=createSignalFunction.test.js.map -------------------------------------------------------------------------------- /src/decorators/effect.ts: -------------------------------------------------------------------------------- 1 | import {effectifyIfNeeded, finalizeMembersIfLast, getMemberStat, getMembers, effects__} from '../_state.js' 2 | import type {AnyObject, ClassySolidMetadata} from './types.js' 3 | import './metadata-shim.js' 4 | 5 | /** 6 | * Decorator for making Solid.js effects out of methods or function-valued 7 | * properties. This is more convenient than calling `this.createEffect()` in the 8 | * constructor or a class method, reducing boilerplate. Pair this with `@signal` 9 | * and `@memo` to make reactive classes with less code. 10 | * 11 | * The `@effect` decorator can be used on methods or auto accessors. Methods are 12 | * the recommended usage. 13 | * 14 | * When used on auto accessors, the auto accessor value must be a function. Note 15 | * that currently the auto accessor function value cannot be changed (if you 16 | * change it, the new function will not be used). 17 | * 18 | * Example: 19 | * 20 | * ```ts 21 | * import { effect, signal, stopEffects } from 'classy-solid' 22 | * import { createSignal } from 'solid-js' 23 | * 24 | * const [a, setA] = createSignal(1) 25 | * 26 | * class Funkalicious { 27 | * @signal b = 2 28 | * 29 | * @effect logSum() { 30 | * console.log('Sum:', a() + this.b) 31 | * } 32 | * 33 | * // Not recommended, but supported (more concise for simple effects): 34 | * @effect accessor logA = () => console.log('a:', a()) 35 | * } 36 | * 37 | * const fun = new Funkalicious() // logs "Sum: 3" 38 | * 39 | * setA(5) // logs "Sum: 7", "a: 5" 40 | * fun.b = 10 // logs "Sum: 15" 41 | * 42 | * // Later, clean up when done... 43 | * stopEffects(fun) 44 | * ``` 45 | * 46 | * When extending from Effectful() or Effects, the `stopEffects` method can 47 | * be used instead of the standalone `stopEffects()` function: 48 | * 49 | * ```ts 50 | * import { effect, signal, Effects } from 'classy-solid' 51 | * import { createSignal } from 'solid-js' 52 | * 53 | * const [a, setA] = createSignal(1) 54 | * 55 | * class Funkalicious extends Effects { 56 | * @signal b = 2 57 | * 58 | * @effect logSum() { 59 | * console.log('Sum:', a() + this.b) 60 | * } 61 | * } 62 | * 63 | * const fun = new Funkalicious() // logs "Sum: 3" 64 | * 65 | * setA(5) // logs "Sum: 7" 66 | * fun.b = 10 // logs "Sum: 15" 67 | * 68 | * // Later, clean up when done... 69 | * fun.stopEffects() 70 | * ``` 71 | */ 72 | 73 | export function effect( 74 | value: Function | ClassAccessorDecoratorTarget void>, 75 | context: ClassMethodDecoratorContext | ClassAccessorDecoratorContext, 76 | ) { 77 | if (context.static) throw new Error('@effect is not supported on static members.') 78 | 79 | const {kind, name} = context 80 | const metadata = context.metadata as ClassySolidMetadata 81 | const signalsAndMemos = getMembers(metadata) 82 | 83 | if (!(kind === 'method' || kind === 'accessor')) 84 | throw new Error('@effect can only be used on methods or function-valued accessors') 85 | 86 | const stat = 87 | kind === 'accessor' 88 | ? getMemberStat(name, 'effect-auto-accessor', signalsAndMemos) 89 | : getMemberStat(name, 'effect-method', signalsAndMemos) 90 | 91 | stat.finalize = function (this: unknown) { 92 | effectifyIfNeeded(this as AnyObject, name, stat) 93 | } 94 | 95 | context.addInitializer(function () { 96 | finalizeMembersIfLast(this as AnyObject, signalsAndMemos) 97 | }) 98 | 99 | if (kind === 'method') stat.value = value 100 | else if (kind === 'accessor') stat.value = (value as ClassAccessorDecoratorTarget void>).get 101 | } 102 | 103 | /** 104 | * Starts all Solid.js effects created by the `@effect` decorator on the given 105 | * object. This can be used to restart effects that were previously stopped with 106 | * `stopEffects()`. 107 | * 108 | * Effects are created and started automatically, so this only needs to be 109 | * called if you have previously stopped the effects and want to start them 110 | * again. 111 | */ 112 | export function startEffects(obj: object) { 113 | let effects = effects__.get(obj as AnyObject) 114 | if (!effects) return 115 | effects.startEffects() 116 | } 117 | 118 | /** 119 | * Stops all Solid.js effects created by the `@effect` decorator on the given 120 | * object. Use this once you are done with the instance and need to clean up. 121 | * 122 | * This does not needed to be used if the object is created in a reactive 123 | * context (such as inside a Solid.js component, or a nested effect), as those 124 | * effects will be cleaned up automatically when the owner context is cleaned 125 | * up. 126 | * 127 | * Effects that have been stopped can later be restarted by calling 128 | * `startEffects(obj)`. 129 | */ 130 | export function stopEffects(obj: object) { 131 | const effects = effects__.get(obj as AnyObject) 132 | if (!effects) return 133 | effects.stopEffects() 134 | } 135 | -------------------------------------------------------------------------------- /src/signals/createSignalFunction.test.ts: -------------------------------------------------------------------------------- 1 | import {createSignalFunction} from './createSignalFunction.js' 2 | 3 | describe('classy-solid', () => { 4 | describe('createSignalFunction()', () => { 5 | it('has gettable and settable values via a single overloaded function', () => { 6 | const num = createSignalFunction(0) 7 | 8 | // Set the variable's value by passing a value in. 9 | num(1) 10 | // Read the variable's value by calling it with no args. 11 | expect(num()).toBe(1) 12 | 13 | // increment example: 14 | const setResult = num(num() + 1) 15 | expect(num()).toBe(2) 16 | expect(setResult).toBe(2) 17 | 18 | // Set with a function that accepts the previous value and returns the next value. 19 | num(n => n + 1) 20 | expect(num()).toBe(3) 21 | }) 22 | 23 | it('works with function values', () => { 24 | const f1 = () => 123 25 | const func = createSignalFunction(f1) 26 | 27 | expect(func()).toBe(f1) 28 | 29 | const f2 = () => 456 30 | func(() => f2) 31 | 32 | expect(func()).toBe(f2) 33 | }) 34 | }) 35 | }) 36 | 37 | ////////////////////////////////////////////////////////////////////////// 38 | // createSignalFunction type tests /////////////////////////////////////// 39 | ////////////////////////////////////////////////////////////////////////// 40 | 41 | { 42 | const num = createSignalFunction(1) 43 | let n1: number = num() 44 | n1 45 | num(123) 46 | num(n => (n1 = n + 1)) 47 | num() 48 | 49 | const num3 = createSignalFunction() 50 | // @ts-expect-error Type 'undefined' is not assignable to type 'number'. ts(2322) 51 | let n3: number = num3() 52 | num3(123) 53 | num3(undefined) // ok, accepts undefined 54 | // @ts-expect-error Object is possibly 'undefined'. ts(2532) (the `n` value) 55 | num3(n => (n3 = n + 1)) 56 | num3() // ok, getter 57 | 58 | // @ts-expect-error Argument of type 'boolean' is not assignable to parameter of type 'number'. ts(2345) 59 | const num4 = createSignalFunction(true) 60 | 61 | const bool = createSignalFunction(true) 62 | let b1: boolean = bool() 63 | b1 64 | bool(false) 65 | bool(b => (b1 = !b)) 66 | bool() 67 | 68 | const bool2 = createSignalFunction() 69 | // @ts-expect-error Type 'undefined' is not assignable to type 'number'. ts(2322) 70 | let n4: boolean = bool2() 71 | bool2(false) 72 | bool2(undefined) // ok, accepts undefined 73 | bool2(n => (n4 = !n)) // ok because undefined is being converted to boolean 74 | // @ts-expect-error Type 'boolean | undefined' is not assignable to type 'boolean'. ts(2322) 75 | bool2(n => (n4 = n)) 76 | bool2() // ok, accepts undefined 77 | 78 | const func = createSignalFunction(() => 1) 79 | // @ts-expect-error 1 is not assignable to function (no overload matches) 80 | func(() => 1) 81 | func(() => (): 1 => 1) // ok, set the value to a function 82 | const fn: () => 1 = func() // ok, returns function value 83 | fn 84 | const n5: 1 = func()() 85 | n5 86 | 87 | const func2 = createSignalFunction<() => number>(() => 1) 88 | // @ts-expect-error number is not assignable to function (no overload matches) 89 | func2(() => 1) 90 | func2(() => () => 1) // ok, set the value to a function 91 | const fn2: () => number = func2() // ok, returns function value 92 | fn2 93 | const n6: number = func2()() 94 | n6 95 | 96 | const stringOrFunc1 = createSignalFunction<(() => number) | string>('') 97 | // @ts-expect-error number not assignable to string | (()=>number) | undefined 98 | stringOrFunc1(() => 1) 99 | const sf1: () => number = stringOrFunc1(() => () => 1) 100 | sf1 101 | const sf2: string = stringOrFunc1('oh yeah') 102 | sf2 103 | const sf3: string = stringOrFunc1(() => 'oh yeah') 104 | sf3 105 | stringOrFunc1() // ok, getter 106 | // @ts-expect-error cannot set signal to undefined 107 | stringOrFunc1(undefined) 108 | // @ts-expect-error return value might be string 109 | const sf6: () => number = stringOrFunc1() 110 | sf6 111 | const sf7: (() => number) | string | undefined = stringOrFunc1() 112 | sf7 113 | const sf8: (() => number) | string = stringOrFunc1() 114 | sf8 115 | 116 | const stringOrFunc2 = createSignalFunction<(() => number) | string>() 117 | // @ts-expect-error number not assignable to string | (()=>number) | undefined 118 | stringOrFunc2(() => 1) 119 | const sf9: () => number = stringOrFunc2(() => () => 1) 120 | sf9 121 | const sf10: string = stringOrFunc2('oh yeah') 122 | sf10 123 | const sf11: string = stringOrFunc2(() => 'oh yeah') 124 | sf11 125 | // @ts-expect-error 'string | (() => number) | undefined' is not assignable to type 'undefined'. 126 | const sf12: undefined = stringOrFunc2() 127 | sf12 128 | const sf13: undefined = stringOrFunc2(undefined) 129 | sf13 130 | const sf14: (() => number) | string | undefined = stringOrFunc2() 131 | sf14 132 | // @ts-expect-error return value might be undefined 133 | const sf15: (() => number) | string = stringOrFunc2() 134 | sf15 135 | } 136 | -------------------------------------------------------------------------------- /dist/decorators/memo.js: -------------------------------------------------------------------------------- 1 | import { finalizeMembersIfLast, getMemberStat, getMembers, memoifyIfNeeded } from '../_state.js'; 2 | import './metadata-shim.js'; 3 | 4 | /** 5 | * A decorator that make a signal property derived from a memoized computation 6 | * based on other signals. Effects depending on this property will re-run when 7 | * the computed value changes, but not if the computed value stays the same even 8 | * if the dependencies changed. 9 | * 10 | * @example 11 | * ```ts 12 | * import {reactive, signal, memo} from "classy-solid"; 13 | * 14 | * class Example { 15 | * @signal a = 1 16 | * @signal b = 2 17 | * 18 | * // @memo can be used on a getter, accessor, or method, with readonly and 19 | * // writable variants: 20 | * 21 | * // Readonly memo via getter only 22 | * @memo get sum1() { 23 | * return this.a + this.b 24 | * } 25 | * 26 | * // Writable memo via getter with setter 27 | * @memo get sum2() { 28 | * return this.a + this.b 29 | * } 30 | * @memo set sum2(value: number) { 31 | * // use an empty setter to enable writable (logic in here will be ignored if any) 32 | * } 33 | * 34 | * // Readonly memo via auto accessor (requires arrow function, not writable because no parameter (arity = 0)) 35 | * @memo accessor sum3 = () => this.a + this.b 36 | * 37 | * // Writable memo via auto accessor (requires arrow function with parameter, arity > 0) 38 | * // Ignore the parameter, it only enables writable memo 39 | * @memo accessor sum4 = (v?: number) => this.a + this.b 40 | * 41 | * // Readonly memo via method 42 | * @memo sum5() { 43 | * return this.a + this.b 44 | * } 45 | * 46 | * // Writable memo via method with parameter (arity > 0) 47 | * @memo sum6(value?: number) { 48 | * // Ignore the parameter, it only enables writable memo 49 | * return this.a + this.b 50 | * } 51 | * 52 | * // The following variants are not supported yet as no runtime or TS support exists yet for the syntax. 53 | * 54 | * // Writable memo via accessor, alternative long-hand syntax (not yet released, no runtime or TS support yet) 55 | * @memo accessor sum6 { get; set } = () => this.a + this.b 56 | * 57 | * // Readonly memo via accessor with only getter (not released yet, no runtime or TS support yet) 58 | * @memo accessor sum7 { get; } = () => this.a + this.b 59 | * 60 | * // Readonly memo via accessor with only getter, alternative syntax (not released yet, no runtime or TS support yet) 61 | * @memo accessor sum8 { 62 | * get() { 63 | * return this.a + this.b 64 | * } 65 | * } 66 | * 67 | * // Readonly memo via accessor with only getter, alternative syntax (not released yet, no runtime or TS support yet) 68 | * @memo accessor sum8 { 69 | * get() { 70 | * return this.a + this.b 71 | * } 72 | * set(_v: number) { 73 | * // empty setter makes it writable (logic in here will be ignored if any) 74 | * } 75 | * } 76 | * } 77 | * 78 | * const ex = new Example() 79 | * 80 | * console.log(ex.sum1, ex.sum2, ex.sum3(), ex.sum4(), ex.sum5(), ex.sum6()) // 3 3 3 3 3 3 81 | * 82 | * createEffect(() => { 83 | * console.log(ex.sum1, ex.sum2, ex.sum3(), ex.sum4(), ex.sum5(), ex.sum6()) 84 | * }); 85 | * 86 | * ex.a = 5 // Logs: 7 7 7 7 7 7 87 | * 88 | * // This won't log anything since the computed memo values don't change (all still 7). 89 | * batch(() => { 90 | * ex.a = 3 91 | * ex.b = 4 92 | * }) 93 | * 94 | * ex.sum2 = 20 // Logs: 20 7 7 7 7 7 (only sum2 is updated) 95 | * 96 | * ex.sum6(15) // Logs: 20 7 7 7 7 15 (only sum6 is updated) 97 | * 98 | * ex.sum1 = 10 // Runtime error: Cannot set readonly property "sum1". 99 | * ``` 100 | */ 101 | export function memo(value, 102 | // today's auto-accessors, writable memo 103 | context) { 104 | if (context.static) throw new Error('@memo is not supported on static fields yet.'); 105 | const { 106 | kind, 107 | name 108 | } = context; 109 | const metadata = context.metadata; 110 | const signalsAndMemos = getMembers(metadata); 111 | 112 | // Apply finalization logic to all except setters (setters are finalized 113 | // together with their getters). 114 | // By skipping setters we also avoid double-counting the getter+setter pair 115 | // in the finalizeMembersIfLast logic. 116 | if (kind === 'setter') return; 117 | 118 | // @ts-expect-error skip type checking in case of invalid kind in plain JS 119 | if (kind === 'field') throw new Error('@memo is not supported on class fields.'); 120 | const memberType = kind === 'accessor' ? 'memo-auto-accessor' : kind === 'method' ? 'memo-method' : 'memo-accessor'; 121 | const stat = getMemberStat(name, memberType, signalsAndMemos); 122 | stat.finalize = function () { 123 | memoifyIfNeeded(this, name, stat); 124 | }; 125 | context.addInitializer(function () { 126 | finalizeMembersIfLast(this, signalsAndMemos); 127 | }); 128 | if (kind === 'method' || kind === 'getter') stat.value = value;else if (kind === 'accessor') stat.value = value.get; 129 | } 130 | //# sourceMappingURL=memo.js.map -------------------------------------------------------------------------------- /dist/decorators/component.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"component.js","names":["Constructor","onMount","createEffect","onCleanup","$TRACK","createMemo","component","Base","context","kind","Error","Class","classComponentWrapper","props","instance","keys","Object","equals","prev","next","length","i","l","prop","ref","template","defineProperties","name","value","configurable","Symbol","hasInstance","obj"],"sources":["../../src/decorators/component.ts"],"sourcesContent":["import {Constructor} from 'lowclass/dist/Constructor.js'\nimport {onMount, createEffect, onCleanup, type JSX, $TRACK, createMemo} from 'solid-js'\nimport './metadata-shim.js'\n\n// https://github.com/ryansolid/dom-expressions/pull/122\n\ninterface PossibleComponent {\n\tonMount?(): void\n\tonCleanup?(): void\n\ttemplate?(props: Record): JSX.Element\n}\ninterface PossiblyReactiveConstructor {}\n\n/**\n * A decorator for using classes as Solid components.\n *\n * Example:\n *\n * ```js\n * ⁣@component\n * class MyComp {\n * ⁣@signal last = 'none'\n *\n * onMount() {\n * console.log('mounted')\n * }\n *\n * template(props) {\n * // here we use `props` passed in, or the signal on `this` which is also\n * // treated as a prop\n * return

Hello, my name is {props.first} {this.last}

\n * }\n * }\n *\n * render(() => )\n * ```\n */\nexport function component(Base: T, context?: DecoratorContext): any {\n\tif (typeof Base !== 'function' || (context && context.kind !== 'class'))\n\t\tthrow new Error('The @component decorator should only be used on a class.')\n\n\tconst Class = Constructor(Base)\n\n\t// Solid only undetstands function components, so we create a wrapper\n\t// function that instantiates the class and hooks up lifecycle methods and\n\t// props.\n\tfunction classComponentWrapper(props: Record): JSX.Element {\n\t\tconst instance = new Class()\n\n\t\tconst keys = createMemo(\n\t\t\t() => {\n\t\t\t\tprops[$TRACK]\n\t\t\t\treturn Object.keys(props)\n\t\t\t},\n\t\t\t[],\n\t\t\t{\n\t\t\t\tequals(prev, next) {\n\t\t\t\t\tif (prev.length !== next.length) return false\n\t\t\t\t\tfor (let i = 0, l = prev.length; i < l; i += 1) if (prev[i] !== next[i]) return false\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tcreateEffect(() => {\n\t\t\t// @ts-expect-error index signature\n\t\t\tfor (const prop of keys()) createEffect(() => (instance[prop] = props[prop]))\n\t\t})\n\n\t\tonMount(() => {\n\t\t\tinstance.onMount?.()\n\n\t\t\tcreateEffect(() => {\n\t\t\t\tconst ref = props.ref as ((component: PossibleComponent) => void) | undefined\n\t\t\t\tref?.(instance)\n\t\t\t})\n\n\t\t\tonCleanup(() => instance.onCleanup?.())\n\t\t})\n\n\t\treturn instance.template?.(props) ?? null\n\t}\n\n\tObject.defineProperties(classComponentWrapper, {\n\t\tname: {\n\t\t\tvalue: Class.name,\n\t\t\tconfigurable: true,\n\t\t},\n\t\t[Symbol.hasInstance]: {\n\t\t\tvalue(obj: unknown) {\n\t\t\t\treturn obj instanceof Class\n\t\t\t},\n\t\t\tconfigurable: true,\n\t\t},\n\t})\n\n\treturn classComponentWrapper\n}\n\ndeclare module 'solid-js' {\n\tnamespace JSX {\n\t\t// Tells JSX what properties class components should have.\n\t\tinterface ElementClass {\n\t\t\ttemplate?(props: Record): JSX.Element\n\t\t}\n\n\t\t// Tells JSX where to look up prop types on class components.\n\t\tinterface ElementAttributesProperty {\n\t\t\tPropTypes: {}\n\t\t}\n\t}\n}\n\nexport type Props = Pick & {\n\tchildren?: JSX.Element\n\tref?: (component: T) => void\n}\n"],"mappings":"AAAA,SAAQA,WAAW,QAAO,8BAA8B;AACxD,SAAQC,OAAO,EAAEC,YAAY,EAAEC,SAAS,EAAYC,MAAM,EAAEC,UAAU,QAAO,UAAU;AACvF,OAAO,oBAAoB;;AAE3B;;AASA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,SAASA,CAAwBC,IAAO,EAAEC,OAA0B,EAAO;EAC1F,IAAI,OAAOD,IAAI,KAAK,UAAU,IAAKC,OAAO,IAAIA,OAAO,CAACC,IAAI,KAAK,OAAQ,EACtE,MAAM,IAAIC,KAAK,CAAC,0DAA0D,CAAC;EAE5E,MAAMC,KAAK,GAAGX,WAAW,CAAiDO,IAAI,CAAC;;EAE/E;EACA;EACA;EACA,SAASK,qBAAqBA,CAACC,KAAuC,EAAe;IACpF,MAAMC,QAAQ,GAAG,IAAIH,KAAK,CAAC,CAAC;IAE5B,MAAMI,IAAI,GAAGV,UAAU,CACtB,MAAM;MACLQ,KAAK,CAACT,MAAM,CAAC;MACb,OAAOY,MAAM,CAACD,IAAI,CAACF,KAAK,CAAC;IAC1B,CAAC,EACD,EAAE,EACF;MACCI,MAAMA,CAACC,IAAI,EAAEC,IAAI,EAAE;QAClB,IAAID,IAAI,CAACE,MAAM,KAAKD,IAAI,CAACC,MAAM,EAAE,OAAO,KAAK;QAC7C,KAAK,IAAIC,CAAC,GAAG,CAAC,EAAEC,CAAC,GAAGJ,IAAI,CAACE,MAAM,EAAEC,CAAC,GAAGC,CAAC,EAAED,CAAC,IAAI,CAAC,EAAE,IAAIH,IAAI,CAACG,CAAC,CAAC,KAAKF,IAAI,CAACE,CAAC,CAAC,EAAE,OAAO,KAAK;QACrF,OAAO,IAAI;MACZ;IACD,CACD,CAAC;IAEDnB,YAAY,CAAC,MAAM;MAClB;MACA,KAAK,MAAMqB,IAAI,IAAIR,IAAI,CAAC,CAAC,EAAEb,YAAY,CAAC,MAAOY,QAAQ,CAACS,IAAI,CAAC,GAAGV,KAAK,CAACU,IAAI,CAAE,CAAC;IAC9E,CAAC,CAAC;IAEFtB,OAAO,CAAC,MAAM;MACba,QAAQ,CAACb,OAAO,GAAG,CAAC;MAEpBC,YAAY,CAAC,MAAM;QAClB,MAAMsB,GAAG,GAAGX,KAAK,CAACW,GAA2D;QAC7EA,GAAG,GAAGV,QAAQ,CAAC;MAChB,CAAC,CAAC;MAEFX,SAAS,CAAC,MAAMW,QAAQ,CAACX,SAAS,GAAG,CAAC,CAAC;IACxC,CAAC,CAAC;IAEF,OAAOW,QAAQ,CAACW,QAAQ,GAAGZ,KAAK,CAAC,IAAI,IAAI;EAC1C;EAEAG,MAAM,CAACU,gBAAgB,CAACd,qBAAqB,EAAE;IAC9Ce,IAAI,EAAE;MACLC,KAAK,EAAEjB,KAAK,CAACgB,IAAI;MACjBE,YAAY,EAAE;IACf,CAAC;IACD,CAACC,MAAM,CAACC,WAAW,GAAG;MACrBH,KAAKA,CAACI,GAAY,EAAE;QACnB,OAAOA,GAAG,YAAYrB,KAAK;MAC5B,CAAC;MACDkB,YAAY,EAAE;IACf;EACD,CAAC,CAAC;EAEF,OAAOjB,qBAAqB;AAC7B","ignoreList":[]} -------------------------------------------------------------------------------- /dist/signals/createSignalObject.test.js: -------------------------------------------------------------------------------- 1 | import { createSignalObject } from './createSignalObject.js'; 2 | describe('classy-solid', () => { 3 | describe('createSignalObject()', () => { 4 | it('has gettable and settable values via .get and .set methods', () => { 5 | const num = createSignalObject(0); 6 | 7 | // Set the variable's value by passing a value in. 8 | num.set(1); 9 | // Read the variable's value by calling it with no args. 10 | expect(num.get()).toBe(1); 11 | 12 | // increment example: 13 | const setResult = num.set(num.get() + 1); 14 | expect(num.get()).toBe(2); 15 | expect(setResult).toBe(2); 16 | 17 | // Set with a function that accepts the previous value and returns the next value. 18 | num.set(n => n + 1); 19 | expect(num.get()).toBe(3); 20 | }); 21 | }); 22 | }); 23 | 24 | ////////////////////////////////////////////////////////////////////////// 25 | // createSignalObject type tests /////////////////////////////////////// 26 | ////////////////////////////////////////////////////////////////////////// 27 | 28 | { 29 | const num = createSignalObject(1); 30 | let n1 = num.get(); 31 | n1; 32 | num.set(123); 33 | num.set(n => n1 = n + 1); 34 | // @ts-expect-error Expected 1 arguments, but got 0. ts(2554) 35 | num.set(); 36 | const num3 = createSignalObject(); 37 | // @ts-expect-error Type 'undefined' is not assignable to type 'number'. ts(2322) 38 | let n3 = num3.get(); 39 | num3.set(123); 40 | num3.set(undefined); // ok, accepts undefined 41 | // @ts-expect-error Object is possibly 'undefined'. ts(2532) (the `n` value) 42 | num3.set(n => n3 = n + 1); 43 | // num3.set() // ok, accepts undefined // FIXME broke in Solid 1.7.9 44 | 45 | // @ts-expect-error Argument of type 'boolean' is not assignable to parameter of type 'number'. ts(2345) 46 | const num4 = createSignalObject(true); 47 | const bool = createSignalObject(true); 48 | let b1 = bool.get(); 49 | b1; 50 | bool.set(false); 51 | bool.set(b => b1 = !b); 52 | // @ts-expect-error Expected 1 arguments, but got 0. ts(2554) 53 | bool.set(); 54 | const bool2 = createSignalObject(); 55 | // @ts-expect-error Type 'undefined' is not assignable to type 'number'. ts(2322) 56 | let n4 = bool2.get(); 57 | bool2.set(false); 58 | bool2.set(undefined); // ok, accepts undefined 59 | bool2.set(n => n4 = !n); // ok because undefined is being converted to boolean 60 | // @ts-expect-error Type 'boolean | undefined' is not assignable to type 'boolean'. ts(2322) 61 | bool2.set(n => n4 = n); 62 | // bool2.set() // ok, accepts undefined // FIXME try Solid 1.7.9 63 | 64 | const func = createSignalObject(() => 1); 65 | // @ts-expect-error 1 is not assignable to function (no overload matches) 66 | func.set(() => 1); 67 | func.set(() => () => 1); // ok, set the value to a function 68 | const fn = func.get(); // ok, returns function value 69 | fn; 70 | const n5 = func.get()(); 71 | n5; 72 | const func2 = createSignalObject(() => 1); 73 | // @FIXME-ts-expect-error number is not assignable to function (no overload matches) 74 | func2.set(() => 1); // FIXME should be a type error. Try Solid 1.7.9 75 | func2.set(() => () => 1); // ok, set the value to a function 76 | const fn2 = func2.get(); // ok, returns function value 77 | fn2; 78 | const n6 = func2.get()(); 79 | n6; 80 | const stringOrFunc1 = createSignalObject(''); 81 | // @FIXME-ts-expect-error number not assignable to string | (()=>number) | undefined 82 | stringOrFunc1.set(() => 1); // FIXME should be a type error. Try Solid 1.7.9 83 | // @ts-expect-error FIXME try Solid 1.7.9 84 | const sf1 = stringOrFunc1.set(() => () => 1); 85 | sf1; 86 | // @ts-expect-error FIXME try Solid 1.7.9 87 | const sf2 = stringOrFunc1.set('oh yeah'); 88 | sf2; 89 | // @ts-expect-error FIXME try Solid 1.7.9 90 | const sf3 = stringOrFunc1.set(() => 'oh yeah'); 91 | sf3; 92 | // @ts-expect-error cannot set signal to undefined 93 | stringOrFunc1.set(); 94 | // @ts-expect-error cannot set signal to undefined 95 | stringOrFunc1.set(undefined); 96 | // @ts-expect-error return value might be string 97 | const sf6 = stringOrFunc1.get(); 98 | sf6; 99 | const sf7 = stringOrFunc1.get(); 100 | sf7; 101 | const sf8 = stringOrFunc1.get(); 102 | sf8; 103 | const stringOrFunc2 = createSignalObject(); 104 | // @FIXME-ts-expect-error number not assignable to string | (()=>number) | undefined 105 | stringOrFunc2.set(() => 1); // FIXME should be a type error. Try Solid 1.7.9 106 | // @ts-expect-error FIXME try Solid 1.7.9 107 | const sf9 = stringOrFunc2.set(() => () => 1); 108 | sf9; 109 | // @ts-expect-error FIXME try Solid 1.7.9 110 | const sf10 = stringOrFunc2.set('oh yeah'); 111 | sf10; 112 | // @ts-expect-error FIXME try Solid 1.7.9 113 | const sf11 = stringOrFunc2.set(() => 'oh yeah'); 114 | sf11; 115 | // @ts-expect-error FIXME try Solid 1.7.9 116 | const sf12 = stringOrFunc2.set(); 117 | sf12; 118 | // @ts-expect-error FIXME try Solid 1.7.9 119 | const sf13 = stringOrFunc2.set(undefined); 120 | sf13; 121 | const sf14 = stringOrFunc2.get(); 122 | sf14; 123 | // @ts-expect-error return value might be undefined 124 | const sf15 = stringOrFunc2.get(); 125 | sf15; 126 | } 127 | //# sourceMappingURL=createSignalObject.test.js.map -------------------------------------------------------------------------------- /logo/Logo-Imagemark_Classy-Dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /logo/Logo-Imagemark_Classy-Light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/signals/createSignalObject.test.ts: -------------------------------------------------------------------------------- 1 | import {createSignalObject} from './createSignalObject.js' 2 | 3 | describe('classy-solid', () => { 4 | describe('createSignalObject()', () => { 5 | it('has gettable and settable values via .get and .set methods', () => { 6 | const num = createSignalObject(0) 7 | 8 | // Set the variable's value by passing a value in. 9 | num.set(1) 10 | // Read the variable's value by calling it with no args. 11 | expect(num.get()).toBe(1) 12 | 13 | // increment example: 14 | const setResult = num.set(num.get() + 1) 15 | expect(num.get()).toBe(2) 16 | expect(setResult).toBe(2) 17 | 18 | // Set with a function that accepts the previous value and returns the next value. 19 | num.set(n => n + 1) 20 | expect(num.get()).toBe(3) 21 | }) 22 | }) 23 | }) 24 | 25 | ////////////////////////////////////////////////////////////////////////// 26 | // createSignalObject type tests /////////////////////////////////////// 27 | ////////////////////////////////////////////////////////////////////////// 28 | 29 | { 30 | const num = createSignalObject(1) 31 | let n1: number = num.get() 32 | n1 33 | num.set(123) 34 | num.set(n => (n1 = n + 1)) 35 | // @ts-expect-error Expected 1 arguments, but got 0. ts(2554) 36 | num.set() 37 | 38 | const num3 = createSignalObject() 39 | // @ts-expect-error Type 'undefined' is not assignable to type 'number'. ts(2322) 40 | let n3: number = num3.get() 41 | num3.set(123) 42 | num3.set(undefined) // ok, accepts undefined 43 | // @ts-expect-error Object is possibly 'undefined'. ts(2532) (the `n` value) 44 | num3.set(n => (n3 = n + 1)) 45 | // num3.set() // ok, accepts undefined // FIXME broke in Solid 1.7.9 46 | 47 | // @ts-expect-error Argument of type 'boolean' is not assignable to parameter of type 'number'. ts(2345) 48 | const num4 = createSignalObject(true) 49 | 50 | const bool = createSignalObject(true) 51 | let b1: boolean = bool.get() 52 | b1 53 | bool.set(false) 54 | bool.set(b => (b1 = !b)) 55 | // @ts-expect-error Expected 1 arguments, but got 0. ts(2554) 56 | bool.set() 57 | 58 | const bool2 = createSignalObject() 59 | // @ts-expect-error Type 'undefined' is not assignable to type 'number'. ts(2322) 60 | let n4: boolean = bool2.get() 61 | bool2.set(false) 62 | bool2.set(undefined) // ok, accepts undefined 63 | bool2.set(n => (n4 = !n)) // ok because undefined is being converted to boolean 64 | // @ts-expect-error Type 'boolean | undefined' is not assignable to type 'boolean'. ts(2322) 65 | bool2.set(n => (n4 = n)) 66 | // bool2.set() // ok, accepts undefined // FIXME try Solid 1.7.9 67 | 68 | const func = createSignalObject(() => 1) 69 | // @ts-expect-error 1 is not assignable to function (no overload matches) 70 | func.set(() => 1) 71 | func.set(() => (): 1 => 1) // ok, set the value to a function 72 | const fn: () => 1 = func.get() // ok, returns function value 73 | fn 74 | const n5: 1 = func.get()() 75 | n5 76 | 77 | const func2 = createSignalObject<() => number>(() => 1) 78 | // @FIXME-ts-expect-error number is not assignable to function (no overload matches) 79 | func2.set(() => 1) // FIXME should be a type error. Try Solid 1.7.9 80 | func2.set(() => () => 1) // ok, set the value to a function 81 | const fn2: () => number = func2.get() // ok, returns function value 82 | fn2 83 | const n6: number = func2.get()() 84 | n6 85 | 86 | const stringOrFunc1 = createSignalObject<(() => number) | string>('') 87 | // @FIXME-ts-expect-error number not assignable to string | (()=>number) | undefined 88 | stringOrFunc1.set(() => 1) // FIXME should be a type error. Try Solid 1.7.9 89 | // @ts-expect-error FIXME try Solid 1.7.9 90 | const sf1: () => number = stringOrFunc1.set(() => () => 1) 91 | sf1 92 | // @ts-expect-error FIXME try Solid 1.7.9 93 | const sf2: string = stringOrFunc1.set('oh yeah') 94 | sf2 95 | // @ts-expect-error FIXME try Solid 1.7.9 96 | const sf3: string = stringOrFunc1.set(() => 'oh yeah') 97 | sf3 98 | // @ts-expect-error cannot set signal to undefined 99 | stringOrFunc1.set() 100 | // @ts-expect-error cannot set signal to undefined 101 | stringOrFunc1.set(undefined) 102 | // @ts-expect-error return value might be string 103 | const sf6: () => number = stringOrFunc1.get() 104 | sf6 105 | const sf7: (() => number) | string | undefined = stringOrFunc1.get() 106 | sf7 107 | const sf8: (() => number) | string = stringOrFunc1.get() 108 | sf8 109 | 110 | const stringOrFunc2 = createSignalObject<(() => number) | string>() 111 | // @FIXME-ts-expect-error number not assignable to string | (()=>number) | undefined 112 | stringOrFunc2.set(() => 1) // FIXME should be a type error. Try Solid 1.7.9 113 | // @ts-expect-error FIXME try Solid 1.7.9 114 | const sf9: () => number = stringOrFunc2.set(() => () => 1) 115 | sf9 116 | // @ts-expect-error FIXME try Solid 1.7.9 117 | const sf10: string = stringOrFunc2.set('oh yeah') 118 | sf10 119 | // @ts-expect-error FIXME try Solid 1.7.9 120 | const sf11: string = stringOrFunc2.set(() => 'oh yeah') 121 | sf11 122 | // @ts-expect-error FIXME try Solid 1.7.9 123 | const sf12: undefined = stringOrFunc2.set() 124 | sf12 125 | // @ts-expect-error FIXME try Solid 1.7.9 126 | const sf13: undefined = stringOrFunc2.set(undefined) 127 | sf13 128 | const sf14: (() => number) | string | undefined = stringOrFunc2.get() 129 | sf14 130 | // @ts-expect-error return value might be undefined 131 | const sf15: (() => number) | string = stringOrFunc2.get() 132 | sf15 133 | } 134 | -------------------------------------------------------------------------------- /dist/decorators/signal.js: -------------------------------------------------------------------------------- 1 | import { batch } from 'solid-js'; 2 | import { getSignal__, trackPropSetAtLeastOnce__ } from '../signals/signalify.js'; 3 | import { isSignalGetter, getMemberStat, finalizeMembersIfLast, getMembers, signalifyIfNeeded } from '../_state.js'; 4 | import './metadata-shim.js'; 5 | const Undefined = Symbol(); 6 | 7 | /** 8 | * @decorator 9 | * Decorate properties of a class with `@signal` to back them with Solid 10 | * signals, making them reactive. 11 | * 12 | * Related: See the Solid.js `createSignal` API for creating standalone signals. 13 | * 14 | * Example: 15 | * 16 | * ```js 17 | * import {signal} from 'classy-solid' 18 | * import {createEffect} from 'solid-js' 19 | * 20 | * class Counter { 21 | * ⁣@signal count = 0 22 | * 23 | * constructor() { 24 | * setInterval(() => this.count++, 1000) 25 | * } 26 | * } 27 | * 28 | * const counter = new Counter() 29 | * 30 | * createEffect(() => { 31 | * console.log('count:', counter.count) 32 | * }) 33 | * ``` 34 | */ 35 | export function signal(value, context) { 36 | if (context.static) throw new Error('@signal is not supported on static fields yet.'); 37 | const { 38 | kind, 39 | name 40 | } = context; 41 | const metadata = context.metadata; 42 | const signalsAndMemos = getMembers(metadata); 43 | if (!(kind === 'field' || kind === 'accessor' || kind === 'getter' || kind === 'setter')) throw new InvalidSignalDecoratorError(); 44 | if (kind === 'field') { 45 | const stat = getMemberStat(name, 'signal-field', signalsAndMemos); 46 | stat.finalize = function () { 47 | signalifyIfNeeded(this, name, stat); 48 | }; 49 | context.addInitializer(function () { 50 | finalizeMembersIfLast(this, signalsAndMemos); 51 | }); 52 | } 53 | 54 | // It's ok that getters/setters/auto-accessors are not finalized the same 55 | // way as with fields above and as with memos/effects, because we do the set 56 | // up during decoration which happens well before any initializers (before 57 | // any memos and effects, so these will be tracked). 58 | else if (kind === 'accessor') { 59 | const { 60 | get, 61 | set 62 | } = value; 63 | const signalStorage = new WeakMap(); 64 | let initialValue = undefined; 65 | const newValue = { 66 | init: function (initialVal) { 67 | initialValue = initialVal; 68 | return initialVal; 69 | }, 70 | get: function () { 71 | getSignal__(this, signalStorage, initialValue)(); 72 | return get.call(this); 73 | }, 74 | set: function (newValue) { 75 | // batch, for example in case setter calls super setter, to 76 | // avoid multiple effect runs on a single property set. 77 | batch(() => { 78 | set.call(this, newValue); 79 | trackPropSetAtLeastOnce__(this, name); // not needed anymore? test it 80 | 81 | const s = getSignal__(this, signalStorage, initialValue); 82 | s(typeof newValue === 'function' ? () => newValue : newValue); 83 | }); 84 | } 85 | }; 86 | isSignalGetter.add(newValue.get); 87 | return newValue; 88 | } else if (kind === 'getter' || kind === 'setter') { 89 | const getOrSet = value; 90 | const initialValue = Undefined; 91 | if (!Object.hasOwn(metadata, 'classySolid_getterSetterSignals')) metadata.classySolid_getterSetterSignals = {}; 92 | const signalsStorages = metadata.classySolid_getterSetterSignals; 93 | let signalStorage = signalsStorages[name]; 94 | if (!signalStorage) signalsStorages[name] = signalStorage = new WeakMap(); 95 | if (!Object.hasOwn(metadata, 'classySolid_getterSetterPairCounts')) metadata.classySolid_getterSetterPairCounts = {}; 96 | const pairs = metadata.classySolid_getterSetterPairCounts; 97 | 98 | // Show a helpful error in case someone forgets to decorate both a getter and setter. 99 | queueMicrotask(() => { 100 | queueMicrotask(() => delete metadata.classySolid_getterSetterPairCounts); 101 | const missing = pairs[name] !== 2; 102 | if (missing) throw new MissingSignalDecoratorError(name); 103 | }); 104 | if (kind === 'getter') { 105 | pairs[name] ??= 0; 106 | pairs[name]++; 107 | const newGetter = function () { 108 | getSignal__(this, signalStorage, initialValue)(); 109 | return getOrSet.call(this); 110 | }; 111 | isSignalGetter.add(newGetter); 112 | return newGetter; 113 | } else { 114 | pairs[name] ??= 0; 115 | pairs[name]++; 116 | const newSetter = function (newValue) { 117 | // batch, for example in case setter calls super setter, to 118 | // avoid multiple effect runs on a single property set. 119 | batch(() => { 120 | getOrSet.call(this, newValue); 121 | trackPropSetAtLeastOnce__(this, name); 122 | const s = getSignal__(this, signalStorage, initialValue); 123 | s(typeof newValue === 'function' ? () => newValue : newValue); 124 | }); 125 | }; 126 | return newSetter; 127 | } 128 | } 129 | } 130 | class MissingSignalDecoratorError extends Error { 131 | constructor(prop) { 132 | super(`Missing @signal decorator on setter or getter for property "${String(prop)}". The @signal decorator will only work on a getter/setter pair with *both* getter and setter decorated with @signal.`); 133 | } 134 | } 135 | class InvalidSignalDecoratorError extends Error { 136 | constructor() { 137 | super('The @signal decorator is only for use on fields, getters, setters, and auto accessors.'); 138 | } 139 | } 140 | //# sourceMappingURL=signal.js.map -------------------------------------------------------------------------------- /dist/effects/createDeferredEffect.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createDeferredEffect.js","names":["createSignal","_createSignal","createEffect","onCleanup","getOwner","runWithOwner","effectQueue","Set","runningEffects","effectDeps","Map","currentEffect","value","options","_get","_set","get","deps","set","add","v","fn","dep","delete","effectTaskIsScheduled","createDeferredEffect","initial","prev","clear","owner","queueMicrotask","runEffects"],"sources":["../../src/effects/createDeferredEffect.ts"],"sourcesContent":["// TODO switch to non-dep-tracking non-queue-modifying deferred signals, because those do not break with regular effects.\n\nimport {createSignal as _createSignal, createEffect, onCleanup, getOwner, runWithOwner} from 'solid-js'\n\nimport type {EffectFunction} from 'solid-js/types/reactive/signal'\n\nconst effectQueue: Set> = new Set()\nlet runningEffects = false\n\n// map of effects to dependencies\nconst effectDeps = new Map, Set<(v: any) => any>>()\nlet currentEffect: EffectFunction = () => {}\n\n// Override createSignal in order to implement custom tracking of effect\n// dependencies, so that when signals change, we are aware which dependenct\n// effects need to be moved to the end of the effect queue while running\n// deferred effects in a microtask.\nexport let createSignal = ((value, options) => {\n\tlet [_get, _set] = _createSignal(value, options)\n\n\tconst get = (() => {\n\t\tif (!runningEffects) return _get()\n\n\t\tlet deps = effectDeps.get(currentEffect)\n\t\tif (!deps) effectDeps.set(currentEffect, (deps = new Set()))\n\t\tdeps.add(_set)\n\n\t\treturn _get()\n\t}) as typeof _get\n\n\tconst set = (v => {\n\t\tif (!runningEffects) return _set(v as any)\n\n\t\t// This is inefficient, for proof of concept, unable to use Solid\n\t\t// internals on the outside.\n\t\tfor (const [fn, deps] of effectDeps) {\n\t\t\tfor (const dep of deps) {\n\t\t\t\tif (dep === _set) {\n\t\t\t\t\t// move to the end\n\t\t\t\t\teffectQueue.delete(fn)\n\t\t\t\t\teffectQueue.add(fn)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn _set(v as any)\n\t}) as typeof _set\n\n\treturn [get, set]\n}) as typeof _createSignal\n\nlet effectTaskIsScheduled = false\n\n// TODO Option so the first run is deferred instead of immediate? This already\n// happens outside of a root.\nexport const createDeferredEffect = ((fn, value, options) => {\n\tlet initial = true\n\n\tcreateEffect(\n\t\t(prev: any) => {\n\t\t\tif (initial) {\n\t\t\t\tinitial = false\n\n\t\t\t\tcurrentEffect = fn\n\t\t\t\teffectDeps.get(fn)?.clear() // clear to track deps, or else it won't track new deps based on code branching\n\t\t\t\tfn(prev)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\teffectQueue.add(fn) // add, or move to the end, of the queue. TODO This is probably redundant now, but I haven't tested yet.\n\n\t\t\t// If we're currently running the queue, return because fn will run\n\t\t\t// again at the end of the queue iteration due to our overriden\n\t\t\t// createSignal moving it to the end.\n\t\t\tif (runningEffects) return\n\n\t\t\tif (effectTaskIsScheduled) return\n\n\t\t\teffectTaskIsScheduled = true\n\n\t\t\tconst owner = getOwner()\n\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tif (owner) runWithOwner(owner, runEffects)\n\t\t\t\telse runEffects()\n\t\t\t})\n\t\t},\n\t\tvalue,\n\t\toptions,\n\t)\n\n\tgetOwner() &&\n\t\tonCleanup(() => {\n\t\t\teffectDeps.delete(fn)\n\t\t\teffectQueue.delete(fn)\n\t\t})\n}) as typeof createEffect\n\nfunction runEffects() {\n\trunningEffects = true\n\n\tfor (const fn of effectQueue) {\n\t\teffectQueue.delete(fn) // TODO This is probably redundant now, but I haven't tested yet.\n\t\tcreateDeferredEffect(fn)\n\t}\n\n\trunningEffects = false\n\teffectTaskIsScheduled = false\n}\n"],"mappings":"AAAA;;AAEA,SAAQA,YAAY,IAAIC,aAAa,EAAEC,YAAY,EAAEC,SAAS,EAAEC,QAAQ,EAAEC,YAAY,QAAO,UAAU;AAIvG,MAAMC,WAAqC,GAAG,IAAIC,GAAG,CAAC,CAAC;AACvD,IAAIC,cAAc,GAAG,KAAK;;AAE1B;AACA,MAAMC,UAAU,GAAG,IAAIC,GAAG,CAA4C,CAAC;AACvE,IAAIC,aAAkC,GAAGA,CAAA,KAAM,CAAC,CAAC;;AAEjD;AACA;AACA;AACA;AACA,OAAO,IAAIX,YAAY,GAAIA,CAACY,KAAK,EAAEC,OAAO,KAAK;EAC9C,IAAI,CAACC,IAAI,EAAEC,IAAI,CAAC,GAAGd,aAAa,CAACW,KAAK,EAAEC,OAAO,CAAC;EAEhD,MAAMG,GAAG,GAAIA,CAAA,KAAM;IAClB,IAAI,CAACR,cAAc,EAAE,OAAOM,IAAI,CAAC,CAAC;IAElC,IAAIG,IAAI,GAAGR,UAAU,CAACO,GAAG,CAACL,aAAa,CAAC;IACxC,IAAI,CAACM,IAAI,EAAER,UAAU,CAACS,GAAG,CAACP,aAAa,EAAGM,IAAI,GAAG,IAAIV,GAAG,CAAC,CAAE,CAAC;IAC5DU,IAAI,CAACE,GAAG,CAACJ,IAAI,CAAC;IAEd,OAAOD,IAAI,CAAC,CAAC;EACd,CAAiB;EAEjB,MAAMI,GAAG,GAAIE,CAAC,IAAI;IACjB,IAAI,CAACZ,cAAc,EAAE,OAAOO,IAAI,CAACK,CAAQ,CAAC;;IAE1C;IACA;IACA,KAAK,MAAM,CAACC,EAAE,EAAEJ,IAAI,CAAC,IAAIR,UAAU,EAAE;MACpC,KAAK,MAAMa,GAAG,IAAIL,IAAI,EAAE;QACvB,IAAIK,GAAG,KAAKP,IAAI,EAAE;UACjB;UACAT,WAAW,CAACiB,MAAM,CAACF,EAAE,CAAC;UACtBf,WAAW,CAACa,GAAG,CAACE,EAAE,CAAC;QACpB;MACD;IACD;IAEA,OAAON,IAAI,CAACK,CAAQ,CAAC;EACtB,CAAiB;EAEjB,OAAO,CAACJ,GAAG,EAAEE,GAAG,CAAC;AAClB,CAA0B;AAE1B,IAAIM,qBAAqB,GAAG,KAAK;;AAEjC;AACA;AACA,OAAO,MAAMC,oBAAoB,GAAIA,CAACJ,EAAE,EAAET,KAAK,EAAEC,OAAO,KAAK;EAC5D,IAAIa,OAAO,GAAG,IAAI;EAElBxB,YAAY,CACVyB,IAAS,IAAK;IACd,IAAID,OAAO,EAAE;MACZA,OAAO,GAAG,KAAK;MAEff,aAAa,GAAGU,EAAE;MAClBZ,UAAU,CAACO,GAAG,CAACK,EAAE,CAAC,EAAEO,KAAK,CAAC,CAAC,EAAC;MAC5BP,EAAE,CAACM,IAAI,CAAC;MAER;IACD;IAEArB,WAAW,CAACa,GAAG,CAACE,EAAE,CAAC,EAAC;;IAEpB;IACA;IACA;IACA,IAAIb,cAAc,EAAE;IAEpB,IAAIgB,qBAAqB,EAAE;IAE3BA,qBAAqB,GAAG,IAAI;IAE5B,MAAMK,KAAK,GAAGzB,QAAQ,CAAC,CAAC;IAExB0B,cAAc,CAAC,MAAM;MACpB,IAAID,KAAK,EAAExB,YAAY,CAACwB,KAAK,EAAEE,UAAU,CAAC,MACrCA,UAAU,CAAC,CAAC;IAClB,CAAC,CAAC;EACH,CAAC,EACDnB,KAAK,EACLC,OACD,CAAC;EAEDT,QAAQ,CAAC,CAAC,IACTD,SAAS,CAAC,MAAM;IACfM,UAAU,CAACc,MAAM,CAACF,EAAE,CAAC;IACrBf,WAAW,CAACiB,MAAM,CAACF,EAAE,CAAC;EACvB,CAAC,CAAC;AACJ,CAAyB;AAEzB,SAASU,UAAUA,CAAA,EAAG;EACrBvB,cAAc,GAAG,IAAI;EAErB,KAAK,MAAMa,EAAE,IAAIf,WAAW,EAAE;IAC7BA,WAAW,CAACiB,MAAM,CAACF,EAAE,CAAC,EAAC;IACvBI,oBAAoB,CAACJ,EAAE,CAAC;EACzB;EAEAb,cAAc,GAAG,KAAK;EACtBgB,qBAAqB,GAAG,KAAK;AAC9B","ignoreList":[]} -------------------------------------------------------------------------------- /src/decorators/memo.ts: -------------------------------------------------------------------------------- 1 | import type {AnyObject, ClassySolidMetadata} from './types.js' 2 | import {finalizeMembersIfLast, getMemberStat, getMembers, memoifyIfNeeded} from '../_state.js' 3 | import './metadata-shim.js' 4 | 5 | /** 6 | * A decorator that make a signal property derived from a memoized computation 7 | * based on other signals. Effects depending on this property will re-run when 8 | * the computed value changes, but not if the computed value stays the same even 9 | * if the dependencies changed. 10 | * 11 | * @example 12 | * ```ts 13 | * import {reactive, signal, memo} from "classy-solid"; 14 | * 15 | * class Example { 16 | * @signal a = 1 17 | * @signal b = 2 18 | * 19 | * // @memo can be used on a getter, accessor, or method, with readonly and 20 | * // writable variants: 21 | * 22 | * // Readonly memo via getter only 23 | * @memo get sum1() { 24 | * return this.a + this.b 25 | * } 26 | * 27 | * // Writable memo via getter with setter 28 | * @memo get sum2() { 29 | * return this.a + this.b 30 | * } 31 | * @memo set sum2(value: number) { 32 | * // use an empty setter to enable writable (logic in here will be ignored if any) 33 | * } 34 | * 35 | * // Readonly memo via auto accessor (requires arrow function, not writable because no parameter (arity = 0)) 36 | * @memo accessor sum3 = () => this.a + this.b 37 | * 38 | * // Writable memo via auto accessor (requires arrow function with parameter, arity > 0) 39 | * // Ignore the parameter, it only enables writable memo 40 | * @memo accessor sum4 = (v?: number) => this.a + this.b 41 | * 42 | * // Readonly memo via method 43 | * @memo sum5() { 44 | * return this.a + this.b 45 | * } 46 | * 47 | * // Writable memo via method with parameter (arity > 0) 48 | * @memo sum6(value?: number) { 49 | * // Ignore the parameter, it only enables writable memo 50 | * return this.a + this.b 51 | * } 52 | * 53 | * // The following variants are not supported yet as no runtime or TS support exists yet for the syntax. 54 | * 55 | * // Writable memo via accessor, alternative long-hand syntax (not yet released, no runtime or TS support yet) 56 | * @memo accessor sum6 { get; set } = () => this.a + this.b 57 | * 58 | * // Readonly memo via accessor with only getter (not released yet, no runtime or TS support yet) 59 | * @memo accessor sum7 { get; } = () => this.a + this.b 60 | * 61 | * // Readonly memo via accessor with only getter, alternative syntax (not released yet, no runtime or TS support yet) 62 | * @memo accessor sum8 { 63 | * get() { 64 | * return this.a + this.b 65 | * } 66 | * } 67 | * 68 | * // Readonly memo via accessor with only getter, alternative syntax (not released yet, no runtime or TS support yet) 69 | * @memo accessor sum8 { 70 | * get() { 71 | * return this.a + this.b 72 | * } 73 | * set(_v: number) { 74 | * // empty setter makes it writable (logic in here will be ignored if any) 75 | * } 76 | * } 77 | * } 78 | * 79 | * const ex = new Example() 80 | * 81 | * console.log(ex.sum1, ex.sum2, ex.sum3(), ex.sum4(), ex.sum5(), ex.sum6()) // 3 3 3 3 3 3 82 | * 83 | * createEffect(() => { 84 | * console.log(ex.sum1, ex.sum2, ex.sum3(), ex.sum4(), ex.sum5(), ex.sum6()) 85 | * }); 86 | * 87 | * ex.a = 5 // Logs: 7 7 7 7 7 7 88 | * 89 | * // This won't log anything since the computed memo values don't change (all still 7). 90 | * batch(() => { 91 | * ex.a = 3 92 | * ex.b = 4 93 | * }) 94 | * 95 | * ex.sum2 = 20 // Logs: 20 7 7 7 7 7 (only sum2 is updated) 96 | * 97 | * ex.sum6(15) // Logs: 20 7 7 7 7 15 (only sum6 is updated) 98 | * 99 | * ex.sum1 = 10 // Runtime error: Cannot set readonly property "sum1". 100 | * ``` 101 | */ 102 | export function memo( 103 | value: 104 | | ((val?: any) => any) // writable memo via field or method 105 | | (() => any) // readonly memo via field or method 106 | | ((val?: any) => void) // memo setter 107 | | (() => void) // memo getter 108 | | ClassAccessorDecoratorTarget any> // today's auto-accessors, readonly memo 109 | | ClassAccessorDecoratorTarget any>, // today's auto-accessors, writable memo 110 | context: 111 | | ClassGetterDecoratorContext 112 | | ClassSetterDecoratorContext 113 | | ClassAccessorDecoratorContext 114 | | ClassMethodDecoratorContext, 115 | ) { 116 | if (context.static) throw new Error('@memo is not supported on static fields yet.') 117 | 118 | const {kind, name} = context 119 | const metadata = context.metadata as ClassySolidMetadata 120 | const signalsAndMemos = getMembers(metadata) 121 | 122 | // Apply finalization logic to all except setters (setters are finalized 123 | // together with their getters). 124 | // By skipping setters we also avoid double-counting the getter+setter pair 125 | // in the finalizeMembersIfLast logic. 126 | if (kind === 'setter') return 127 | 128 | // @ts-expect-error skip type checking in case of invalid kind in plain JS 129 | if (kind === 'field') throw new Error('@memo is not supported on class fields.') 130 | 131 | const memberType = kind === 'accessor' ? 'memo-auto-accessor' : kind === 'method' ? 'memo-method' : 'memo-accessor' 132 | 133 | const stat = getMemberStat(name, memberType, signalsAndMemos) 134 | 135 | stat.finalize = function (this: unknown) { 136 | memoifyIfNeeded(this as AnyObject, name, stat) 137 | } 138 | 139 | context.addInitializer(function () { 140 | finalizeMembersIfLast(this as AnyObject, signalsAndMemos) 141 | }) 142 | 143 | if (kind === 'method' || kind === 'getter') stat.value = value 144 | else if (kind === 'accessor') stat.value = (value as ClassAccessorDecoratorTarget void>).get 145 | } 146 | -------------------------------------------------------------------------------- /src/decorators/signal.ts: -------------------------------------------------------------------------------- 1 | import {batch} from 'solid-js' 2 | import {getSignal__, trackPropSetAtLeastOnce__} from '../signals/signalify.js' 3 | import type {AnyObject, ClassySolidMetadata} from './types.js' 4 | import type {SignalFunction} from '../signals/createSignalFunction.js' 5 | import {isSignalGetter, getMemberStat, finalizeMembersIfLast, getMembers, signalifyIfNeeded} from '../_state.js' 6 | import './metadata-shim.js' 7 | 8 | const Undefined = Symbol() 9 | 10 | /** 11 | * @decorator 12 | * Decorate properties of a class with `@signal` to back them with Solid 13 | * signals, making them reactive. 14 | * 15 | * Related: See the Solid.js `createSignal` API for creating standalone signals. 16 | * 17 | * Example: 18 | * 19 | * ```js 20 | * import {signal} from 'classy-solid' 21 | * import {createEffect} from 'solid-js' 22 | * 23 | * class Counter { 24 | * ⁣@signal count = 0 25 | * 26 | * constructor() { 27 | * setInterval(() => this.count++, 1000) 28 | * } 29 | * } 30 | * 31 | * const counter = new Counter() 32 | * 33 | * createEffect(() => { 34 | * console.log('count:', counter.count) 35 | * }) 36 | * ``` 37 | */ 38 | export function signal( 39 | value: unknown, 40 | context: 41 | | ClassFieldDecoratorContext 42 | | ClassGetterDecoratorContext 43 | | ClassSetterDecoratorContext 44 | | ClassAccessorDecoratorContext, 45 | ): any { 46 | if (context.static) throw new Error('@signal is not supported on static fields yet.') 47 | 48 | const {kind, name} = context 49 | const metadata = context.metadata as ClassySolidMetadata 50 | const signalsAndMemos = getMembers(metadata) 51 | 52 | if (!(kind === 'field' || kind === 'accessor' || kind === 'getter' || kind === 'setter')) 53 | throw new InvalidSignalDecoratorError() 54 | 55 | if (kind === 'field') { 56 | const stat = getMemberStat(name, 'signal-field', signalsAndMemos) 57 | 58 | stat.finalize = function (this: unknown) { 59 | signalifyIfNeeded(this as AnyObject, name, stat) 60 | } 61 | 62 | context.addInitializer(function () { 63 | finalizeMembersIfLast(this as AnyObject, signalsAndMemos) 64 | }) 65 | } 66 | 67 | // It's ok that getters/setters/auto-accessors are not finalized the same 68 | // way as with fields above and as with memos/effects, because we do the set 69 | // up during decoration which happens well before any initializers (before 70 | // any memos and effects, so these will be tracked). 71 | else if (kind === 'accessor') { 72 | const {get, set} = value as {get: () => unknown; set: (v: unknown) => void} 73 | const signalStorage = new WeakMap>() 74 | let initialValue: unknown = undefined 75 | 76 | const newValue = { 77 | init: function (this: object, initialVal: unknown) { 78 | initialValue = initialVal 79 | return initialVal 80 | }, 81 | get: function (this: object): unknown { 82 | getSignal__(this, signalStorage, initialValue)() 83 | return get.call(this) 84 | }, 85 | set: function (this: object, newValue: unknown) { 86 | // batch, for example in case setter calls super setter, to 87 | // avoid multiple effect runs on a single property set. 88 | batch(() => { 89 | set.call(this, newValue) 90 | trackPropSetAtLeastOnce__(this, name) // not needed anymore? test it 91 | 92 | const s = getSignal__(this, signalStorage, initialValue) 93 | s(typeof newValue === 'function' ? () => newValue : newValue) 94 | }) 95 | }, 96 | } 97 | 98 | isSignalGetter.add(newValue.get) 99 | 100 | return newValue 101 | } else if (kind === 'getter' || kind === 'setter') { 102 | const getOrSet = value as Function 103 | const initialValue = Undefined 104 | 105 | if (!Object.hasOwn(metadata, 'classySolid_getterSetterSignals')) metadata.classySolid_getterSetterSignals = {} 106 | const signalsStorages = metadata.classySolid_getterSetterSignals! 107 | 108 | let signalStorage = signalsStorages[name] 109 | if (!signalStorage) signalsStorages[name] = signalStorage = new WeakMap>() 110 | 111 | if (!Object.hasOwn(metadata, 'classySolid_getterSetterPairCounts')) metadata.classySolid_getterSetterPairCounts = {} 112 | const pairs = metadata.classySolid_getterSetterPairCounts! 113 | 114 | // Show a helpful error in case someone forgets to decorate both a getter and setter. 115 | queueMicrotask(() => { 116 | queueMicrotask(() => delete metadata.classySolid_getterSetterPairCounts) 117 | const missing = pairs[name] !== 2 118 | if (missing) throw new MissingSignalDecoratorError(name) 119 | }) 120 | 121 | if (kind === 'getter') { 122 | pairs[name] ??= 0 123 | pairs[name]++ 124 | 125 | const newGetter = function (this: object): unknown { 126 | getSignal__(this, signalStorage, initialValue)() 127 | return getOrSet.call(this) 128 | } 129 | 130 | isSignalGetter.add(newGetter) 131 | 132 | return newGetter 133 | } else { 134 | pairs[name] ??= 0 135 | pairs[name]++ 136 | 137 | const newSetter = function (this: object, newValue: unknown) { 138 | // batch, for example in case setter calls super setter, to 139 | // avoid multiple effect runs on a single property set. 140 | batch(() => { 141 | getOrSet.call(this, newValue) 142 | trackPropSetAtLeastOnce__(this, name) 143 | 144 | const s = getSignal__(this, signalStorage, initialValue) 145 | s(typeof newValue === 'function' ? () => newValue : newValue) 146 | }) 147 | } 148 | 149 | return newSetter 150 | } 151 | } 152 | } 153 | 154 | class MissingSignalDecoratorError extends Error { 155 | constructor(prop: PropertyKey) { 156 | super( 157 | `Missing @signal decorator on setter or getter for property "${String( 158 | prop, 159 | )}". The @signal decorator will only work on a getter/setter pair with *both* getter and setter decorated with @signal.`, 160 | ) 161 | } 162 | } 163 | 164 | class InvalidSignalDecoratorError extends Error { 165 | constructor() { 166 | super('The @signal decorator is only for use on fields, getters, setters, and auto accessors.') 167 | } 168 | } 169 | --------------------------------------------------------------------------------