Skip to content

Commit

Permalink
update autoKey plugin with babel
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardoperra committed Nov 5, 2024
1 parent 58712a8 commit a163544
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 44 deletions.
33 changes: 23 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@

<img alt="StateBuilder" src="./banner.png">

> **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)

`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

Expand All @@ -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**

Expand All @@ -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**
Expand Down Expand Up @@ -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';
Expand All @@ -87,6 +94,12 @@ export default defineConfig({
plugins: [
statebuilder({
autoKey: true,
filterStores: [
// define your custom store name
]
experimental: {
transformStateProviderDirective: true
}
}),
],
});
Expand Down
43 changes: 30 additions & 13 deletions packages/state/vite/autoKey.ts
Original file line number Diff line number Diff line change
@@ -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,
};
},
};
}
39 changes: 39 additions & 0 deletions packages/state/vite/babel/astAutoNaming.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,51 @@
import * as t from '@babel/types';

export interface BabelAstAddAutoNamingOptions {
filterStores: (callee: string) => boolean;
}

export function babelAstAddAutoNaming({
filterStores,
}: BabelAstAddAutoNamingOptions): babel.PluginObj<any> {
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,
): boolean {
if (program.type !== 'Program') {
return false;
}

let modify = false;
for (const statement of program.body) {
t.traverse(statement, (node, ancestors) => {
Expand Down
27 changes: 27 additions & 0 deletions packages/state/vite/babel/transform.ts
Original file line number Diff line number Diff line change
@@ -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<babel.TransformOptions['parserOpts']>['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,
});
}
24 changes: 4 additions & 20 deletions packages/state/vite/stateProviderDirective.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -11,26 +12,9 @@ export function stateProviderDirective(): Plugin {
if (code.indexOf('use stateprovider') === -1) {
return;
}
const plugins: NonNullable<
NonNullable<babel.TransformOptions['parserOpts']>['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 || '',
Expand Down
2 changes: 1 addition & 1 deletion packages/state/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ export default defineConfig({
// threads: false,
// isolate: false,
},
plugins: [solidPlugin(), tsconfigPaths(), statebuilder()],
plugins: [solidPlugin(), tsconfigPaths()],
});

0 comments on commit a163544

Please sign in to comment.