Skip to content

Commit

Permalink
expression proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
mrgrain committed Dec 17, 2024
1 parent fc36f4f commit c5e36f2
Showing 1 changed file with 188 additions and 0 deletions.
188 changes: 188 additions & 0 deletions packages/@cdklabs/typewriter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,194 @@ const blockStmt = code.stmt.block(
);
```

## Expression Proxy (`$E`)

The Expression Proxy (`$E`) is a mechanism that allows you to build expression trees by intercepting JavaScript operations and converting them into an AST (Abstract Syntax Tree). It provides a more natural way to write code generation expressions.

### Basic Usage

```typescript
import { $E, code } from '@cdklabs/typewriter';

// Create a proxy-wrapped expression
const expression = $E(someExpression);

// Method calls and property access will be converted to expression nodes
expression.someMethod(); // Becomes a method call expression
expression.someProperty; // Becomes a property access expression
```

### How It Works

The Expression Proxy works through JavaScript's Proxy mechanism and handles three main types of operations:

1. **Property Access**: Accessing properties on the proxy creates property access expressions
2. **Method Calls**: Calling methods creates call expressions
3. **Construction**: Using `new` creates new instance expressions

### Example Usage

```ts
import { $E, code, Module, Type, TypeScriptRenderer } from '@cdklabs/typewriter';

const scope = new Module('example');

// Create a function that uses expression proxies
const fn = scope.addFunction({
name: 'example',
returnType: Type.STRING,
parameters: [{ name: 'value', type: 'number' }],
returnType: 'string',
});

// Using $E to create method chains
fn.addBody(
code.stmt.ret(
$E(code.expr.ident('value')).toString()
)
);

// Renders as:
// function example(value: number): string {
// return value.toString();
// }
```

### Key Features & best practices

1. Automatic Expression Building

```ts
// The proxy automatically converts operations into expression nodes
const proxy = $E(someExpression);
proxy.method(); // Creates a method call expression
proxy.property; // Creates a property access expression
new proxy(); // Creates a new instance expression
```

2. Chaining Support

```ts
// Method chains are automatically converted to nested expressions
$E(baseExpression)
.method1()
.method2()
.property;
```

3. Use for Dynamic Expressions

```ts
// Good: Using $E for dynamic method calls
$E(baseExpression).dynamicMethod();

// Less ideal: Manual expression building
code.expr.call(code.expr.prop(baseExpression, 'dynamicMethod'));
```

4. Combine with Static Code Generation

```ts
// Mixing static and dynamic expressions
code.stmt.ret(
$E(code.expr.lit('hello'))
.toUpperCase()
.concat($E(code.expr.lit(' world')))
);

```

5. Maintains Type Safety

```ts
// The proxy maintains type information from the original expression
const typedExpression = $E(code.expr.lit(42));
// TypeScript will provide proper type hints for number methods
```

### Limitations

- Cannot intercept JavaScript operators (like `+`, `-`, `*`, etc.)
- Only handles method calls, property access, and construction operations
- May make code generation logic less explicit compared to direct expression building

### When to Use

Use Expression Proxy when:

- Building complex method chains
- Working with dynamic property access
- Creating fluent interfaces in generated code
- Simplifying expression tree construction

Use direct expression building when:

- Working with operators
- Needing explicit control over the expression tree
- Building simple, static expressions

This feature is particularly useful when generating code that involves method chaining or complex property access patterns, as it provides a more natural and readable way to construct expression trees compared to manually building them using the expression builder API.

## TypeScriptRenderer

The `TypeScriptRenderer` is a crucial component of the @cdklabs/typewriter library, responsible for generating TypeScript code from the abstract syntax tree (AST) you've built using the library's constructs.

### Purpose

The `TypeScriptRenderer` takes the code objects you've created (such as modules, classes, interfaces, and functions) and transforms them into valid TypeScript code. It handles the intricacies of proper indentation, syntax, and structure, ensuring that the output is correct and readable.

### Usage

Here's an example of how to use the `TypeScriptRenderer`:

```ts
import { Module, TypeScriptRenderer, FreeFunction, code } from '@cdklabs/typewriter';

// Create a scope
const scope = new Module('myModule');

// Add a function to the scope
const myFunction = new FreeFunction(scope, {
name: 'greet',
parameters: [{ name: 'name', type: 'string' }],
returnType: 'string',
});

// Add function body
myFunction.addBody(
code.stmt.ret(code.expr.strConcat(code.expr.lit('Hello, '), code.expr.ident('name'), code.expr.lit('!')))
);

// Create a renderer instance
const renderer = new TypeScriptRenderer();

// Render the scope
const generatedCode = renderer.render(scope);

console.log(generatedCode);
// Output:
// function greet(name: string): string {
// return "Hello, " + name + "!";
// }
```

### ESLint Rule Management

The `TypeScriptRenderer` also provides functionality for managing ESLint rules in the generated code. This is particularly useful when you need to disable certain linting rules for generated code. By default, the `prettier/prettier` and `max-len` ESLint rules are disabled.

You can customize the ESLint rule management using the `eslintRules` option when creating a `TypeScriptRenderer` instance:

```ts
const renderer = new TypeScriptRenderer({
eslintRules: {
'@typescript-eslint/comma-dangle': 'off',
'max-len': 'off',
},
});
```

This feature allows you to fine-tune the linting behavior for your generated code, ensuring it meets your project's coding standards while accommodating the nature of generated code.

## General Usage Recommendations

1. Start with a `Module`: Always begin by creating a `Module` instance, which will serve as the root of your code structure.
Expand Down

0 comments on commit c5e36f2

Please sign in to comment.