An open API service indexing awesome lists of open source software.

https://github.com/unstoppablecarl/unplugin-inline

an unplugin to inline pure functions
https://github.com/unstoppablecarl/unplugin-inline

esbuild-plugins performance tsup-plugin unplugin vite-plugin webpack-plugin

Last synced: 3 months ago
JSON representation

an unplugin to inline pure functions

Awesome Lists containing this project

README

          

# unplugin-inline

AST-driven unplugin that inlines pure functions at build time across Vite, Rollup, Webpack, and esbuild.

---

## Why

V8 refuses to inline functions whose bytecode exceeds ~460 instructions or ~600 bytes, even on the hot path—every call still pays full frame
setup cost at runtime. Marking a function with `/* @__INLINE__ */` moves that cost to build time instead, flattening the
logic directly into the caller.

## Installation

```bash
pnpm add -D unplugin-inline
# or
npm install -D unplugin-inline
# or
yarn add -D unplugin-inline
```

## Example Usage

### 1. Standard Block Inlining (`@__INLINE__`)

Consider a physics simulation where `transformPoint` is called millions of times per frame. The function is large enough
that V8 refuses to inline it automatically.

#### Source Code

**`src/physics.ts`**

```ts
/* @__INLINE__ */
function transformPoint(x: number, y: number, z: number, matrix: Float32Array): number {
const m = matrix;
const tx = m[0] * x + m[4] * y + m[8] * z + m[12]
const ty = m[1] * x + m[5] * y + m[9] * z + m[13]
const tz = m[2] * x + m[6] * y + m[10] * z + m[14]
const tw = m[3] * x + m[7] * y + m[11] * z + m[15]

return Math.sqrt(tx * tx + ty * ty + tz * tz) / tw
}

// Called millions of times per frame — no function call overhead in the output
export function processVertices(vertices: Float32Array, matrix: Float32Array): number {
let sum = 0
for (let i = 0; i < vertices.length; i += 3) {
sum += transformPoint(vertices[i], vertices[i + 1], vertices[i + 2], matrix)
}
return sum
}
```

#### Compiled Output

The compiled output has no `transformPoint` declaration. Its body is placed directly at the call site as a flat labeled
block:

**`dist/physics.js`**

```js
function processVertices(vertices, matrix) {
let sum = 0;
for (let i = 0; i < vertices.length; i += 3) {
let _transformPointResult;
_transformPointLabel: {
const _x = vertices[i];
const _y = vertices[i + 1];
const _z = vertices[i + 2];
const _matrix = matrix;
const tx = _matrix[0] * _x + _matrix[4] * _y + _matrix[8] * _z + _matrix[12];
const ty = _matrix[1] * _x + _matrix[5] * _y + _matrix[9] * _z + _matrix[13];
const tz = _matrix[2] * _x + _matrix[6] * _y + _matrix[10] * _z + _matrix[14];
const tw = _matrix[3] * _x + _matrix[7] * _y + _matrix[11] * _z + _matrix[15];
_transformPointResult = Math.sqrt(tx * tx + ty * ty + tz * tz) / tw;
}
sum += _transformPointResult;
}
return sum;
}
```

#### 2. Macro Expression Inlining (`@__INLINE_MACRO__`)

For extremely hot, small math utility functions, generating block scopes can bloat the bundle and create unnecessary extra variables. Using `@__INLINE_MACRO__` bypasses block generation entirely, performing a direct AST expression substitution wrapped in parentheses to preserve operator precedence.

**`src/math.ts`**

```ts
/** @__INLINE_MACRO__ */
const blendAlpha = (a: number, b: number) => (a * b + 128) >> 8;

export const color = blendAlpha(100, 255) * 2;
```

**`dist/math.js`**

```js
export const color = (((100) * (255) + 128) >> 8) * 2;
```

*Both block (`/* @__INLINE__ */`) and line (`// @__INLINE__`) comment styles are recognized.*

## Bundler Configuration

### Vite

**`vite.config.ts`**

```ts
import { defineConfig } from 'vite'
import { vitePlugin } from 'unplugin-inline'

export default defineConfig({
plugins: [vitePlugin()]
})
```

### esbuild

**`build.js`**

```js
import esbuild from 'esbuild'
import { esbuildPlugin } from 'unplugin-inline'

esbuild.build({
entryPoints: ['input.js'],
bundle: true,
plugins: [esbuildPlugin()],
})
```

### tsup

**`tsup.config.ts`**

```ts
import { defineConfig } from 'tsup'
import { esbuildPlugin } from 'unplugin-inline'

export default defineConfig({
esbuildPlugins: [esbuildPlugin()],
})
```

### Rollup

**`rollup.config.js`**

```js
import { rollupPlugin } from 'unplugin-inline'

export default {
input: 'input.js',
plugins: [rollupPlugin()],
}
```

### Webpack

**`webpack.config.js`**

```js
const { webpackPlugin } = require('unplugin-inline')

module.exports = {
plugins: [webpackPlugin()],
}
```

## Configuration Options

| Option | Type | Default | Description |
|----------------------------|------------|-----------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `inlineIdentifier` | `string` | `'@__INLINE__'` | The comment string used to mark functions for standard block inlining. |
| `inlineMacroIdentifier` | `string` | `'@__INLINE_MACRO__'` | The comment string used to mark functions for direct AST expression substitution (Macros). |
| `autoConvertInlineToMacro` | `boolean` | `true` | If `true`, the plugin will attempt to automatically upgrade standard `@__INLINE__` functions to macros if they meet all safety requirements, falling back to block-scoping if they don't or if passed impure arguments. |
| `allowedGlobals` | `string[]` | [See Defaults](https://www.google.com/search?q=src/defaults.ts) | Global identifiers available inside inlined functions. |
| `fileExtensions` | `string[]` | [See Defaults](https://www.google.com/search?q=src/defaults.ts) | File extensions the plugin will process. |

## ⚠️ Requirements & Restrictions

A function must be **pure** to be inlinable. The plugin enforces strict AST analysis and will throw build errors if you
violate these rules:

* **No async or generators** — `async`/`await` and `function*` alter execution timing.
* **No `this` or `arguments**` — both bind to the caller after inlining, producing unpredictable results.
* **No outer scope mutations** — cannot reassign variables declared outside the function's own block.
* **No outer scope references** — cannot read variables from an outer scope. Standard globals (`Math`, `JSON`, etc.) are
permitted — see `allowedGlobals` for the full default list.
* **No recursive functions** — recursion cannot be unrolled at the call site.
* **No call expressions in conditionals** — the function must be called as a standalone statement or direct assignment,
not inside a ternary or `if` condition. Assign the result first:

```js
// ❌ Not allowed
if (processValue(x) > 100) { // ...
}

// ✅ Assign first, then use
const processed = processValue(x)
if (processed > 100) { // ...
}
```

### Specific Requirements for Macros (`@__INLINE_MACRO__`)

Because macros perform direct AST substitution rather than creating a lexical block scope, they have additional strict requirements:

* **Must resolve to a single pure expression** — Macros cannot contain multiple statements, variable declarations, or block-level logic (like `if` statements). Arrow functions with implicit returns or standard functions with a single `return` statement are allowed.
* **No Side-Effect Duplication (Multiple Evaluation Bug)** — If an argument with side-effects (e.g., `i++`, `Math.random()`, `getPixel()`) is passed into a macro, the plugin analyzes how many times that parameter is referenced in the macro body. If the parameter is referenced more than once, expanding the macro would cause the side-effect to be evaluated multiple times. The build will throw a safety validation error.

## Benchmarks

```bash
npm run bench
```

## Release Automation

* update `package.json` file version (example: `1.0.99`)
* manually create a github release with a tag matching the `package.json` version prefixed with `v` (example: `v1.0.99`)
* npm should be updated automatically

## License

MIT