diff --git a/README.md b/README.md index fe68c0e..ba10c48 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ StateBuilder -> **Warning** This library has been built for experimental purposes for my needs while building apps that need an +> [!WARNING] +> +> This library has been built for experimental purposes for my needs while building apps that need an > agnostic state manager and a certain complexity. [![NPM](https://img.shields.io/npm/v/statebuilder?style=for-the-badge)](https://www.npmjs.com/package/statebuilder) @@ -10,13 +12,12 @@ `StateBuilder` is an agnostic state management library built on the top of SolidJS reactivity. It's built to be an **extremely modular system**, with an API that allows you to add methods, utilities and custom -behaviors to your store -in an easier way. Of course, this come with a built-in TypeScript support. +behaviors to your store in an easier way. Of course, this come with a built-in TypeScript support. Solid already provides the primitives to build a state manager system thanks to signals and stores. What's missing is a well-defined pattern to follow while building your application. -Thanks to `StateBuilder` you can **compose** the approach you like to handle your state. +Thanks to `StateBuilder` you can **compose** the approach you like to handle your state. You can also use some patterns already exposed by the library. ## Table of contents @@ -27,12 +28,10 @@ Thanks to `StateBuilder` you can **compose** the approach you like to handle you ## Architecture -`StateBuilder` come to the rescue introducing some concepts: - ### **State container** The state container it's a plain JavaScript object that collects all resolved store instances. Once created, every state -container will have it's own reactive scope, defined by the `Owner` object from solid-js API. +container will have it's own reactive scope, bound into a `Owner` from solid-js API. ### **Store definition creator** @@ -44,7 +43,7 @@ specific signature to be complaint to `StateBuilder` API. - defineStore - defineSignal -Using the store definition creator, you can define your state business logic, which will be +Using the store definition, you can define your state business logic, which will be **lazy evaluated** once the state is injected the first time. ### **Plugin** @@ -74,9 +73,17 @@ Install `StateBuilder` by running the following command of the following: pnpm i statebuilder # or npm or yarn ``` -### Enable compiler / plugin (Vite Only) +### Enable compiler (Vite Only) -If you're using Vite with SolidJS, you can use the `statebuilder` custom plugin, which allows to debug easily in dev. +> [!NOTE] +> +> The statebuilder plugin is OPTIONAL. This means that all the core features works right out of the +> box without a custom build step + +If you're using Vite with SolidJS, you can use the `statebuilder` custom plugin, which provide debug and custom features through babel transforms. + +- `autoKey`: Allows to name your stores automatically, based on the constant name. +- `stateProviderDirective`: Allows to wraps your SolidJS component into a StateProvider when they contains the `use stateprovider` directive. ```ts import { defineConfig } from 'vite'; @@ -87,6 +94,12 @@ export default defineConfig({ plugins: [ statebuilder({ autoKey: true, + filterStores: [ + // define your custom store name + ] + experimental: { + transformStateProviderDirective: true + } }), ], }); diff --git a/packages/state/vite/autoKey.ts b/packages/state/vite/autoKey.ts index 382863c..d36f62f 100644 --- a/packages/state/vite/autoKey.ts +++ b/packages/state/vite/autoKey.ts @@ -1,28 +1,45 @@ +import * as babel from '@babel/core'; +import { basename } from 'node:path'; import { Plugin } from 'vite'; -import { astAddAutoNaming } from './babel/astAutoNaming'; -import { parseModule } from 'magicast'; +import { + babelAstAddAutoNaming, + BabelAstAddAutoNamingOptions, +} from './babel/astAutoNaming'; +import { transformAsync } from './babel/transform'; interface StatebuilderAutonamingOptions { transformStores: string[]; } export function autoKey(options: StatebuilderAutonamingOptions): Plugin { const { transformStores } = options; + const findStoresTransformRegexp = new RegExp(transformStores.join('|')); return { name: 'statebuilder:autokey', - transform(code, id, options) { - if (!code.includes('statebuilder')) { + async transform(code, id, options) { + if ( + !code.includes('statebuilder') || + !findStoresTransformRegexp.test(code) + ) { return; } - const findStoresTransformRegexp = new RegExp(transformStores.join('|')); - if (findStoresTransformRegexp.test(code)) { - const module = parseModule(code); - const result = astAddAutoNaming(module.$ast, (functionName) => - transformStores.includes(functionName), - ); - if (result) { - return module.generate(); - } + const result = await transformAsync(id, code, [ + [ + babelAstAddAutoNaming, + { + filterStores: (functionName) => + transformStores.includes(functionName), + } as BabelAstAddAutoNamingOptions, + ], + ]); + + if (!result) { + return; } + + return { + code: result.code ?? '', + map: result.map, + }; }, }; } diff --git a/packages/state/vite/babel/astAutoNaming.ts b/packages/state/vite/babel/astAutoNaming.ts index 1469af9..84d9f63 100644 --- a/packages/state/vite/babel/astAutoNaming.ts +++ b/packages/state/vite/babel/astAutoNaming.ts @@ -1,5 +1,43 @@ import * as t from '@babel/types'; +export interface BabelAstAddAutoNamingOptions { + filterStores: (callee: string) => boolean; +} + +export function babelAstAddAutoNaming({ + filterStores, +}: BabelAstAddAutoNamingOptions): babel.PluginObj { + return { + name: 'statebuilder:stateprovider-addAutoName', + visitor: { + CallExpression(path) { + if ( + t.isIdentifier(path.node.callee) && + filterStores(path.node.callee.name) + ) { + const ancestors = path.getAncestry(); + ancestorsLoop: { + let variableName: string | null = null; + for (const ancestor of ancestors) { + if ( + t.isVariableDeclarator(ancestor.node) && + t.isIdentifier(ancestor.node.id) + ) { + variableName = ancestor.node.id.name; + const storeOptions = t.objectExpression([ + createNameProperty(variableName!), + ]); + path.node.arguments.push(storeOptions); + break ancestorsLoop; + } + } + } + } + }, + }, + }; +} + export function astAddAutoNaming( program: t.Node, filterStores: (name: string) => boolean, @@ -7,6 +45,7 @@ export function astAddAutoNaming( if (program.type !== 'Program') { return false; } + let modify = false; for (const statement of program.body) { t.traverse(statement, (node, ancestors) => { diff --git a/packages/state/vite/babel/transform.ts b/packages/state/vite/babel/transform.ts new file mode 100644 index 0000000..0473050 --- /dev/null +++ b/packages/state/vite/babel/transform.ts @@ -0,0 +1,27 @@ +import * as babel from '@babel/core'; +import { basename } from 'node:path'; + +export function transformAsync( + moduleId: string, + code: string, + customPlugins: babel.TransformOptions['plugins'], +) { + const plugins: NonNullable< + NonNullable['plugins'] + > = ['jsx']; + if (/\.[mc]?tsx?$/i.test(moduleId)) { + plugins.push('typescript'); + } + return babel.transformAsync(code, { + plugins: customPlugins, + parserOpts: { + plugins, + }, + filename: basename(moduleId), + ast: false, + sourceMaps: true, + configFile: false, + babelrc: false, + sourceFileName: moduleId, + }); +} diff --git a/packages/state/vite/stateProviderDirective.ts b/packages/state/vite/stateProviderDirective.ts index 39827fd..cc444a0 100644 --- a/packages/state/vite/stateProviderDirective.ts +++ b/packages/state/vite/stateProviderDirective.ts @@ -2,6 +2,7 @@ import * as babel from '@babel/core'; import { basename } from 'node:path'; import { Plugin } from 'vite'; import { babelReplaceStateProviderDirective } from './babel/replaceStateProviderDirective'; +import { transformAsync } from './babel/transform'; export function stateProviderDirective(): Plugin { return { @@ -11,26 +12,9 @@ export function stateProviderDirective(): Plugin { if (code.indexOf('use stateprovider') === -1) { return; } - const plugins: NonNullable< - NonNullable['plugins'] - > = ['jsx']; - if (/\.[mc]?tsx?$/i.test(id)) { - plugins.push('typescript'); - } - - const result = await babel.transformAsync(code, { - plugins: [[babelReplaceStateProviderDirective]], - parserOpts: { - plugins, - }, - filename: basename(id), - ast: false, - sourceMaps: true, - configFile: false, - babelrc: false, - sourceFileName: id, - }); - + const result = await transformAsync(id, code, [ + [babelReplaceStateProviderDirective], + ]); if (result) { return { code: result.code || '', diff --git a/packages/state/vitest.config.ts b/packages/state/vitest.config.ts index f07eb7a..aa30506 100644 --- a/packages/state/vitest.config.ts +++ b/packages/state/vitest.config.ts @@ -14,5 +14,5 @@ export default defineConfig({ // threads: false, // isolate: false, }, - plugins: [solidPlugin(), tsconfigPaths(), statebuilder()], + plugins: [solidPlugin(), tsconfigPaths()], });