Skip to content
This repository has been archived by the owner on Oct 9, 2024. It is now read-only.

Commit

Permalink
feat: Make the babel plugin configurable through options (#27)
Browse files Browse the repository at this point in the history
* feat: Make the babel plugin configurable through options

* Add changeset
  • Loading branch information
mohebifar authored Mar 9, 2024
1 parent b492032 commit 829a728
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changeset/hungry-frogs-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-unforget/babel-plugin": minor
---

Make the babel plugin configurable through options
8 changes: 8 additions & 0 deletions apps/docs/pages/usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,11 @@ module.exports = {
},
};
```

## Options

The babel plugin accepts a few options:

- `throwOnFailure` (default: `false`): If `true`, the plugin will throw an error if it fails to analyze the component / hook.
- `skipComponents` (default: `[]`): An array of component names to skip. For example, `["MyComponent"]`.
- `skipComponentsWithMutation` (default: `false`): If `true`, the plugin will skip components that have variables that are mutated e.g. `array.push()` or variable re-assignment. This is configurable because despite thorough testing, we are still not 100% sure that this is safe to do in all cases.
24 changes: 23 additions & 1 deletion packages/babel-plugin/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
# `@react-unforget/compiler`
# `@react-unforget/babel-plugin`

To use the babel plugin, you need to install it and add it to your Babel configuration.

```sh
npm install --save-dev @react-unforget/babel-plugin
```

Then, add it to your Babel configuration. For example, in your `.babelrc`:

```json
{
"plugins": ["@react-unforget/babel-plugin"]
}
```

## Options

The babel plugin accepts a few options:

- `throwOnFailure` (default: `false`): If `true`, the plugin will throw an error if it fails to analyze the component / hook.
- `skipComponents` (default: `[]`): An array of component names to skip. For example, `["MyComponent"]`.
- `skipComponentsWithMutation` (default: `false`): If `true`, the plugin will skip components that have variables that are mutated e.g. `array.push()` or variable re-assignment. This is configurable because despite thorough testing, we are still not 100% sure that this is safe to do in all cases.
16 changes: 12 additions & 4 deletions packages/babel-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import type * as babel from "@babel/core";
import { visitProgram } from "~/visit-program";
import { findComponents } from "~/utils/path-tools/find-components";
import type { Options } from "~/models/TransformationContext";
import { makeTransformationContext } from "~/models/TransformationContext";

export { visitProgram, findComponents };

export interface Options {}

export default function asyncBabelPlugin() {
export default function unforgetBabelPlugin(
_: object,
options: Options,
): babel.PluginObj {
const transformationContext = makeTransformationContext({
throwOnFailure: options.throwOnFailure ?? false,
skipComponents: options.skipComponents ?? [],
skipComponentsWithMutation: options.skipComponentsWithMutation ?? false,
});
return {
visitor: {
Program(path: babel.NodePath<babel.types.Program>) {
visitProgram(path);
visitProgram(path, transformationContext);
},
},
};
Expand Down
20 changes: 19 additions & 1 deletion packages/babel-plugin/src/models/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { Binding } from "@babel/traverse";
import { isControlFlowStatement } from "~/utils/path-tools/control-flow-utils";
import { isInTheSameFunctionScope } from "~/utils/path-tools/is-in-the-same-function-scope";
import { ComponentSegment } from "./segment/ComponentSegment";
import type { TransformationContext } from "./TransformationContext";

export class Component {
/* Cache props */
Expand All @@ -25,7 +26,11 @@ export class Component {
private rootSegment: ComponentSegment | null = null;
private cacheIdToName = new Map<number, string>();

constructor(public path: babel.NodePath<t.Function>) {
constructor(
public path: babel.NodePath<t.Function>,
public name: string,
public transformationContext?: TransformationContext,
) {
path.assertFunction();

this.cacheValueIdentifier = path.scope.generateUidIdentifier(
Expand Down Expand Up @@ -80,7 +85,20 @@ export class Component {
bodySegment.setParent(this.rootSegment);
}

private hasMutation() {
return Array.from(this.segmentsMap.values()).some(
(segment) => segment.getMutationDependencies().size > 0,
);
}

applyTransformation() {
if (
this.transformationContext?.skipComponentsWithMutation &&
this.hasMutation()
) {
return;
}

this.rootSegment?.applyTransformation();
const cacheVariableDeclaration = this.makeCacheVariableDeclaration();
this.getFunctionBody().unshiftContainer("body", cacheVariableDeclaration);
Expand Down
24 changes: 24 additions & 0 deletions packages/babel-plugin/src/models/TransformationContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface Options {
throwOnFailure?: boolean;
skipComponents?: string[];
skipComponentsWithMutation?: boolean;
}

export function makeTransformationContext({
throwOnFailure = true,
skipComponents = [],
skipComponentsWithMutation = false,
}: Options = {}) {
return {
throwOnFailure,
skipComponents,
skipComponentsWithMutation,
shouldSkipComponent(componentName: string) {
return skipComponents.includes(componentName);
},
};
}

export type TransformationContext = ReturnType<
typeof makeTransformationContext
>;
35 changes: 28 additions & 7 deletions packages/babel-plugin/src/utils/path-tools/find-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import type * as babel from "@babel/core";
import * as t from "@babel/types";
import { Component } from "~/models/Component";
import { doesMatchHookName } from "~/utils/ast-tools/is-hook-call";
import type { TransformationContext } from "~/models/TransformationContext";
import { getReturnsOfFunction } from "./get-returns-of-function";

export function findComponents(program: babel.NodePath<babel.types.Program>) {
export function findComponents(
program: babel.NodePath<babel.types.Program>,
transformationContext?: TransformationContext,
) {
const components: Component[] = [];

program.traverse({
Expand All @@ -25,18 +29,27 @@ export function findComponents(program: babel.NodePath<babel.types.Program>) {
}
}

const nameMatchesComponentName = id && doesIdMatchComponentName(id.name);
const nameMatchesHook = id && doesMatchHookName(id.name);
const name = id?.name;
const nameMatchesComponentName = name && doesIdMatchComponentName(name);
const nameMatchesHook = name && doesMatchHookName(name);

if (!nameMatchesComponentName && !nameMatchesHook) {
if (!nameMatchesComponentName && !nameMatchesHook && !name) {
return;
}

if (transformationContext?.shouldSkipComponent(name)) {
return;
}

if (path.isArrowFunctionExpression()) {
const body = path.get("body");
if (!body.isBlockStatement() && isComponentReturnType(body.node)) {
components.push(
new Component(path as babel.NodePath<babel.types.Function>),
new Component(
path as babel.NodePath<babel.types.Function>,
name,
transformationContext,
),
);
return;
}
Expand All @@ -50,7 +63,11 @@ export function findComponents(program: babel.NodePath<babel.types.Program>) {

if (nameMatchesHook) {
components.push(
new Component(path as babel.NodePath<babel.types.Function>),
new Component(
path as babel.NodePath<babel.types.Function>,
name,
transformationContext,
),
);
}

Expand Down Expand Up @@ -78,7 +95,11 @@ export function findComponents(program: babel.NodePath<babel.types.Program>) {

if (allReturnsMatch) {
components.push(
new Component(path as babel.NodePath<babel.types.Function>),
new Component(
path as babel.NodePath<babel.types.Function>,
name,
transformationContext,
),
);
}
},
Expand Down
11 changes: 1 addition & 10 deletions packages/babel-plugin/src/utils/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import * as babel from "@babel/core";
import * as generateBase from "@babel/generator";
import traverse from "@babel/traverse";
import type * as t from "@babel/types";
import { visitProgram } from "~/visit-program";
import { mermaidGraphFromComponent } from "~/utils/misc/mermaid-graph-from-component";
import babelPlugin from "../index";

const babelTransformOptions = {
plugins: [
["@babel/plugin-transform-typescript", { isTSX: true }],
babelPlugin,
[babelPlugin, { throwOnFailure: true }],
],
} satisfies babel.TransformOptions;

Expand All @@ -35,14 +34,6 @@ export function transformForJest(input: string, filename: string) {
return code;
}

export function transformWithParseAndCast(input: string) {
const root = parse(input);

visitProgram(root);

return String(root);
}

export function parse(input: string) {
const ast = babel.parse(input, babelTransformOptions)!;

Expand Down
20 changes: 16 additions & 4 deletions packages/babel-plugin/src/visit-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,29 @@ import {
RUNTIME_MODULE_CREATE_CACHE_HOOK_NAME,
RUNTIME_MODULE_NAME,
} from "./utils/constants";
import type { TransformationContext } from "./models/TransformationContext";

export function visitProgram(path: babel.NodePath<babel.types.Program>) {
const components = findComponents(path);
export function visitProgram(
path: babel.NodePath<babel.types.Program>,
context?: TransformationContext,
) {
const components = findComponents(path, context);

if (components.length === 0) {
return;
}

components.forEach((component) => {
component.analyze();
component.applyTransformation();
try {
component.analyze();
component.applyTransformation();
} catch (e) {
if (!context || context?.throwOnFailure) {
throw e;
} else {
console.warn(`Failed to transform component ${component.name}`, e);
}
}
});

const useCacheHookIdentifier = t.identifier(
Expand Down

0 comments on commit 829a728

Please sign in to comment.