Composition API pattern implementation for vanilla JavaScript libraries with context injection, async support, and namespace management.
—
Build-time transformation plugins automatically transform async functions to preserve context across await statements, enabling universal async context support without runtime dependencies.
/**
* Universal bundler plugin for async context transformation
* Created with createUnplugin, provides bundler-specific methods
*/
export const unctxPlugin: {
rollup(options?: UnctxPluginOptions): Plugin;
vite(options?: UnctxPluginOptions): Plugin;
webpack(options?: UnctxPluginOptions): Plugin;
};
/**
* Create AST transformer for async context preservation
* @param options - Transformation configuration
* @returns Transformer with transform and shouldTransform methods
*/
function createTransformer(options?: TransformerOptions): {
transform(code: string, options?: { force?: false }): TransformResult | undefined;
shouldTransform(code: string): boolean;
};
interface UnctxPluginOptions extends TransformerOptions {
/**
* Filter function to determine which files to transform
* @param id - File path/identifier
* @returns true if file should be transformed
*/
transformInclude?: (id: string) => boolean;
}
interface TransformerOptions {
/**
* Function names to be transformed for async context preservation
* @default ['withAsyncContext']
*/
asyncFunctions?: string[];
/**
* Module name to import helper functions from
* @default 'unctx'
*/
helperModule?: string;
/**
* Name of the helper function for async execution
* @default 'executeAsync'
*/
helperName?: string;
/**
* Object property transformation rules
* Map function names to property names to transform
* @default {}
*/
objectDefinitions?: Record<string, string[]>;
}
interface TransformResult {
code: string;
magicString: MagicString;
}// vite.config.js
import { unctxPlugin } from "unctx/plugin";
export default {
plugins: [
unctxPlugin.vite({
// Transform functions named 'withAsyncContext' and 'callAsync'
asyncFunctions: ['withAsyncContext', 'callAsync'],
// Only transform specific files
transformInclude: (id) => {
return id.includes('src/') && !id.includes('node_modules/');
}
})
]
};// rollup.config.js
import { unctxPlugin } from "unctx/plugin";
export default {
plugins: [
unctxPlugin.rollup({
asyncFunctions: ['withAsyncContext', 'callAsync'],
transformInclude: (id) => id.endsWith('.ts') || id.endsWith('.js')
})
]
};// webpack.config.js
const { unctxPlugin } = require("unctx/plugin");
module.exports = {
plugins: [
unctxPlugin.webpack({
asyncFunctions: ['withAsyncContext'],
helperModule: 'unctx',
helperName: 'executeAsync'
})
]
};import { createContext, withAsyncContext } from "unctx";
const userContext = createContext<User>();
const processUser = withAsyncContext(async () => {
console.log(userContext.use()); // Available
await fetch("/api/data");
console.log(userContext.use()); // Would be undefined without transformation
await processMoreData();
return userContext.use();
});The plugin automatically transforms the code to:
import { executeAsync as __executeAsync } from "unctx";
import { createContext, withAsyncContext } from "unctx";
const userContext = createContext<User>();
const processUser = withAsyncContext(async () => {
let __temp, __restore;
console.log(userContext.use()); // Available
(([__temp, __restore] = __executeAsync(() => fetch("/api/data"))),
__temp = await __temp, __restore(), __temp);
console.log(userContext.use()); // ✅ Context restored automatically
(([__temp, __restore] = __executeAsync(() => processMoreData())),
__temp = await __temp, __restore(), __temp);
return userContext.use();
}, 1);Enable callAsync transformation by adding it to asyncFunctions:
// Build configuration
unctxPlugin.vite({
asyncFunctions: ['withAsyncContext', 'callAsync']
})import { createContext } from "unctx";
const ctx = createContext<Data>();
// Now callAsync is automatically transformed
await ctx.callAsync(data, async () => {
console.log(ctx.use()); // Available
await operation1();
console.log(ctx.use()); // ✅ Restored automatically
await operation2();
console.log(ctx.use()); // ✅ Restored automatically
});Transform your own async functions:
// Build configuration
unctxPlugin.vite({
asyncFunctions: ['withAsyncContext', 'myAsyncWrapper', 'customAsync']
})// Define custom async wrapper
function myAsyncWrapper<T>(fn: () => Promise<T>): () => Promise<T> {
return fn; // Actual implementation
}
// Usage - will be transformed
const handler = myAsyncWrapper(async () => {
const ctx = myContext.use();
await asyncOperation();
return myContext.use(); // Context preserved
});Transform specific properties within object definitions:
// Build configuration
unctxPlugin.vite({
objectDefinitions: {
'defineMiddleware': ['handler'],
'defineRoute': ['middleware', 'handler']
}
})// These object properties will be transformed
const middleware = defineMiddleware({
handler: async (req, res) => {
const ctx = requestContext.use();
await processRequest();
// Context automatically restored
return requestContext.use();
}
});
const route = defineRoute({
path: '/api/users',
middleware: async (req, res, next) => {
await authenticate();
// Context preserved
next();
},
handler: async (req, res) => {
await handleRequest();
// Context preserved
}
});Create custom transformers for advanced use cases:
import { createTransformer } from "unctx/transform";
const transformer = createTransformer({
asyncFunctions: ['myAsyncFn'],
helperModule: 'my-context-lib',
helperName: 'executeWithContext'
});
// Check if code needs transformation
const code = `
const handler = myAsyncFn(async () => {
await fetch('/api');
});
`;
if (transformer.shouldTransform(code)) {
const result = transformer.transform(code);
console.log(result.code); // Transformed code
}The plugin works seamlessly with TypeScript:
// tsconfig.json - no special configuration needed
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext"
}
}// TypeScript code is transformed correctly
interface UserData {
id: string;
name: string;
}
const userContext = createContext<UserData>();
const processUser = withAsyncContext(async (): Promise<UserData> => {
const user = userContext.use(); // Typed correctly
await validateUser(user);
return userContext.use(); // TypeScript knows this is UserData
});Control which files are transformed:
unctxPlugin.vite({
transformInclude: (id) => {
// Only transform application code, skip node_modules
if (id.includes('node_modules/')) return false;
// Only transform specific file types
if (!id.match(/\.(ts|js|vue)$/)) return false;
// Only transform files in src directory
return id.includes('/src/');
}
})// Optimize build performance
unctxPlugin.vite({
// Limit transformation to necessary functions
asyncFunctions: ['withAsyncContext'], // Don't include unnecessary functions
// Use precise file filtering
transformInclude: (id) => {
// Skip large third-party files
if (id.includes('node_modules/')) return false;
// Only transform files that actually use unctx
return id.includes('src/') && !id.includes('.spec.');
}
})The transformation adds minimal runtime overhead:
// Generated code is efficient
const result = (([__temp, __restore] = __executeAsync(() => operation())),
__temp = await __temp, __restore(), __temp);
// Equivalent to manual context restoration but automatedunctxPlugin.vite({
// Source maps are generated automatically
// Use browser dev tools to debug original code
})import { createTransformer } from "unctx/transform";
const transformer = createTransformer();
const result = transformer.transform(sourceCode);
console.log('Transformed code:');
console.log(result.code);// ❌ Function name not in asyncFunctions list
const handler = myCustomAsync(async () => {
// Context lost after await
});
// ✅ Add function name to config
unctxPlugin.vite({
asyncFunctions: ['withAsyncContext', 'myCustomAsync']
})// ❌ Missing import for generated helper
// The plugin automatically adds: import { executeAsync as __executeAsync } from "unctx";
// ✅ Ensure unctx is installed as dependency
// npm install unctx// ❌ Variable assignment prevents transformation
const handler = withAsyncContext;
const myHandler = handler(async () => {
// Not transformed - indirect call
});
// ✅ Direct function call enables transformation
const myHandler = withAsyncContext(async () => {
// Transformed correctly
});Install with Tessl CLI
npx tessl i tessl/npm-unctx