CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-unctx

Composition API pattern implementation for vanilla JavaScript libraries with context injection, async support, and namespace management.

Pending
Overview
Eval results
Files

build-plugins.mddocs/

Build-time Transformation

Build-time transformation plugins automatically transform async functions to preserve context across await statements, enabling universal async context support without runtime dependencies.

API Reference

/**
 * 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;
}

Plugin Installation

Vite

// 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

// 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

// webpack.config.js
const { unctxPlugin } = require("unctx/plugin");

module.exports = {
  plugins: [
    unctxPlugin.webpack({
      asyncFunctions: ['withAsyncContext'],
      helperModule: 'unctx',
      helperName: 'executeAsync'
    })
  ]
};

Transformation Process

Before Transformation

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();
});

After Transformation

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);

Using callAsync with Transformation

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
});

Custom Function Transformation

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
});

Object Property Transformation

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
  }
});

Manual Transformer Usage

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
}

TypeScript Integration

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
});

File Filtering

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/');
  }
})

Performance Considerations

Build Performance

// 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.');
  }
})

Runtime Performance

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 automated

Debugging Transformed Code

Enable Source Maps

unctxPlugin.vite({
  // Source maps are generated automatically
  // Use browser dev tools to debug original code
})

Inspect Transformed Output

import { createTransformer } from "unctx/transform";

const transformer = createTransformer();
const result = transformer.transform(sourceCode);

console.log('Transformed code:');
console.log(result.code);

Common Issues and Solutions

Issue: Functions Not Being Transformed

// ❌ Function name not in asyncFunctions list
const handler = myCustomAsync(async () => {
  // Context lost after await
});

// ✅ Add function name to config
unctxPlugin.vite({
  asyncFunctions: ['withAsyncContext', 'myCustomAsync']
})

Issue: TypeScript Compilation Errors

// ❌ Missing import for generated helper
// The plugin automatically adds: import { executeAsync as __executeAsync } from "unctx";

// ✅ Ensure unctx is installed as dependency
// npm install unctx

Issue: Context Still Lost

// ❌ 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

docs

async-context.md

build-plugins.md

context-creation.md

index.md

namespace-management.md

tile.json