A build-time transformer that converts JavaScript/TypeScript directives into higher-order function calls. Transform your code's behavior declaratively using simple string directives.
directive-to-hof enables you to use declarative directives in your functions that get transformed into higher-order function calls at build time. This approach provides compile-time optimization without runtime overhead.
- Zero Runtime Overhead: Directives are transformed at build time, not runtime
- Multiple Directive Support: Use multiple directives on the same function
- Collision-Safe Imports: Automatic import name collision resolution
- TypeScript Support: Full TypeScript compatibility with type safety
- Build Tool Integration: Works with Vite, Rollup, and esbuild
- Flexible Configuration: Support for async-only directives and custom import paths
npm install directive-to-hofyarn add directive-to-hofpnpm add directive-to-hof// Before transformation
function expensiveCalculation(x, y) {
'use once';
'use memo';
return x * y * Math.random();
}
// After transformation
import { useOnce } from './use-once-function.js';
import { useMemo } from './memo-function.js';
const expensiveCalculation = useOnce(
useMemo((x, y) => {
return x * y * Math.random();
})
);import { defineConfig } from 'vite';
import { vite } from 'directive-to-hof';
export default defineConfig({
plugins: [
vite({
directive: 'use once',
importPath: './use-once-function.js',
importName: 'useOnce',
asyncOnly: false,
}),
],
});import { rollup } from 'directive-to-hof';
export default {
plugins: [
rollup({
directive: 'use once',
importPath: './use-once-function.js',
importName: 'useOnce',
asyncOnly: false,
}),
],
};import { esbuild } from 'directive-to-hof';
const plugins = [
esbuild({
directive: 'use once',
importPath: './use-once-function.js',
importName: 'useOnce',
asyncOnly: false,
}),
];interface DirectiveTransformerOptions {
/**
* The directive string to look for in function bodies
*/
directive: string;
/**
* Path to the module containing the higher-order function
*/
importPath: string;
/**
* Name of the higher-order function to import
*/
importName: string;
/**
* Whether the directive can only be used in async functions
* @default true
*/
asyncOnly?: boolean;
}You can configure multiple directives by passing an array of options:
const transformer = createDirectiveTransformer([
{
directive: 'use once',
importPath: './use-once-function.js',
importName: 'useOnce',
asyncOnly: false,
},
{
directive: 'use memo',
importPath: './memo-function.js',
importName: 'useMemo',
asyncOnly: false,
},
{
directive: 'use cache',
importPath: './cache-function.js',
importName: 'useCache',
asyncOnly: true,
},
]);import { createDirectiveTransformer } from 'directive-to-hof';
const transformer = createDirectiveTransformer({
directive: 'use once',
importPath: './use-once-function.js',
importName: 'useOnce',
asyncOnly: false,
});
const code = `
let count = 0;
function increment() {
'use once';
return ++count;
}
`;
const { contents } = await transformer(code, { path: './example.js' });
console.log(contents);// use-once-function.js
export function useOnce(fn) {
let result;
let called = false;
return (...args) => {
if (called) return result;
called = true;
return (result = fn(...args));
};
}// memo-function.js
export function useMemo(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}// cache-function.js
export function useCache(fn) {
const cache = new Map();
return async (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = await fn(...args);
cache.set(key, result);
return result;
};
}- Function declarations
- Function expressions
- Arrow functions
- Object methods
- Class methods
// Input
function complexFunction(x, y) {
'use once';
'use memo';
return x * y * Math.random();
}
// Output
const complexFunction = useOnce(
useMemo((x, y) => {
return x * y * Math.random();
})
);The transformer automatically handles import name collisions by generating unique aliases:
// If 'useOnce' already exists in scope
const useOnce = 'existing variable';
// The transformer will generate a unique import alias
import { useOnce as useOnce_ABC123 } from './use-once-function.js';Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
This project is licensed under the MIT License - see the LICENSE file for details.
See CHANGELOG.md for a list of changes and version history.