triggerkit
Version:
A plugin to connect SvelteKit functions to your trigger.dev tasks
1,002 lines (1,001 loc) • 49.5 kB
JavaScript
import fs from "node:fs";
import path from 'node:path';
const VIRTUAL_MODULE_ID = 'virtual:triggerkit';
const NAMESPACE = 'triggerkit-namespace';
function triggerkit(options) {
const resolvedOptions = {
includeDirs: options?.includeDirs || ["src/lib"],
filePatterns: options?.filePatterns || [".ts", ".js"],
exclude: options?.exclude || ["test.", "spec.", ".d.ts"],
exportStrategy: {
mode: "individual",
groupBy: "folder",
...options?.exportStrategy
},
includeTypes: {
functions: true,
classes: true,
constants: false,
variables: false,
...options?.includeTypes
},
debugLevel: options?.debugLevel || "minimal"
};
// Keep track of file states for rebuilding
const fileCache = new Map();
const envVars = new Set();
return {
name: "triggerkit",
onBuildStart: async (context) => {
if (resolvedOptions.debugLevel !== "off") {
context.logger.log("🚀 Triggerkit extension starting!");
}
if (resolvedOptions.debugLevel === "verbose") {
context.logger.log("📋 Export strategy:", resolvedOptions.exportStrategy.mode);
context.logger.log("🎯 Include types:", Object.entries(resolvedOptions.includeTypes).filter(([_, v]) => v).map(([k, _]) => k).join(', '));
}
// Load environment variables from SvelteKit's .env files
await loadSvelteKitEnvironment(context, resolvedOptions.debugLevel);
// Clear the file cache to start fresh
fileCache.clear();
envVars.clear();
// Scan each folder for function files
for (const includeDir of resolvedOptions.includeDirs) {
const dirPath = path.resolve(context.workingDir, includeDir);
// Check if the folder exists
if (!fs.existsSync(dirPath)) {
if (resolvedOptions.debugLevel !== "off") {
context.logger.warn(`⚠️ Directory ${includeDir} does not exist, skipping`);
}
continue;
}
await scanFolderForEnvVars(dirPath, resolvedOptions.filePatterns, resolvedOptions.exclude, envVars, context, resolvedOptions.debugLevel);
// Scan the folder recursively
await scanFolderForExports(dirPath, resolvedOptions.filePatterns, resolvedOptions.exclude, fileCache, context, resolvedOptions.includeTypes);
}
// Debug: Log all cached files
if (resolvedOptions.debugLevel === "verbose") {
context.logger.log("📝 Files in cache:");
for (const [filePath, cachedFile] of fileCache.entries()) {
const exportedItems = extractExportedItems(cachedFile.content || '', resolvedOptions.includeTypes);
const exportNames = exportedItems.map(item => `${item.name}(${item.type})`);
context.logger.log(` - ${filePath}: [${exportNames.join(', ')}]`);
}
}
// Only proceed if we have files to process
if (fileCache.size === 0) {
if (resolvedOptions.debugLevel !== "off") {
context.logger.warn("❌ No export files found, triggerkit will not be active");
}
return;
}
if (resolvedOptions.debugLevel !== "off") {
context.logger.log(`✅ Found ${fileCache.size} export files`);
}
// Create and register an esbuild plugin
const triggerKitPlugin = {
name: 'virtual-triggerkit-module',
setup(build) {
if (resolvedOptions.debugLevel === "verbose") {
context.logger.log("🔧 Setting up esbuild plugin");
}
// Enhance module resolution
build.onResolve({ filter: /^src\// }, (args) => {
// Resolve paths relative to the working directory
let resolvedPath = path.resolve(context.workingDir, args.path);
const extensions = ['.ts', '.js'];
if (!fs.existsSync(resolvedPath)) {
for (const ext of extensions) {
const pathWithExt = resolvedPath + ext;
if (fs.existsSync(pathWithExt)) {
resolvedPath = pathWithExt;
break;
}
}
}
return { path: resolvedPath };
});
// Handle requests for our virtual module
build.onResolve({ filter: /^virtual:triggerkit$/ }, (args) => {
if (resolvedOptions.debugLevel === "verbose") {
context.logger.log("🎯 VIRTUAL MODULE REQUESTED!", args);
}
if (fileCache.size === 0) {
if (resolvedOptions.debugLevel !== "off") {
context.logger.warn("❌ No export files found when resolving virtual module");
}
return { errors: [{ text: 'No export files found' }] };
}
if (resolvedOptions.debugLevel === "verbose") {
context.logger.log("✅ Resolving virtual:triggerkit module");
}
return {
path: VIRTUAL_MODULE_ID,
namespace: NAMESPACE
};
});
// Handle SvelteKit env imports
build.onResolve({ filter: /^\$env\/static\/(public|private)$/ }, () => {
return {
path: VIRTUAL_MODULE_ID,
namespace: NAMESPACE
};
});
// Handle loading the virtual module
build.onLoad({ filter: /.*/, namespace: NAMESPACE }, async () => {
if (resolvedOptions.debugLevel === "verbose") {
context.logger.log("📦 LOADING VIRTUAL MODULE CONTENT");
}
if (fileCache.size === 0) {
if (resolvedOptions.debugLevel !== "off") {
context.logger.warn("❌ No export files found when loading virtual module");
}
return { errors: [{ text: 'No export files found' }] };
}
// Generate virtual module content based on export strategy
const moduleContent = generateVirtualModuleContent(fileCache, envVars, resolvedOptions, context);
if (resolvedOptions.debugLevel === "verbose") {
context.logger.log("📝 Generated module content (first 500 chars):");
context.logger.log(moduleContent.substring(0, 500));
}
return {
contents: moduleContent,
loader: 'ts',
};
});
},
};
// Register the plugin
context.registerPlugin(triggerKitPlugin);
context.logger.log("✅ Plugin registered");
// Generate type declarations
generateFunctionTypeDeclarations(context, fileCache);
// Add function files as a layer to ensure they're included in the build
const layerFiles = {};
for (const [filePath, _] of fileCache.entries()) {
const relativePath = path.relative(context.workingDir, filePath);
layerFiles[relativePath] = filePath;
}
context.addLayer({
id: "triggerkit-layer",
files: layerFiles
});
context.logger.log("🎉 Triggerkit setup complete!");
}
};
}
// Enhanced extraction to support classes
function extractExportedItems(content, includeTypes) {
const exportedItems = [];
// Function patterns
if (includeTypes.functions) {
const functionPatterns = [
// export function name<T>(params): ReturnType or export async function name<T>(params): ReturnType
/export\s+(async\s+)?function\s+([a-zA-Z0-9_$]+)(\s*<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^{;]+))?/g,
// export const name = (params): ReturnType => or export const name = async (params): ReturnType =>
/export\s+const\s+([a-zA-Z0-9_$]+)\s*=\s*(async\s+)?\(([^)]*)\)\s*:\s*([^=>{]+)\s*=>/g,
// export const name: Type = function or export const name: Type = async function
/export\s+const\s+([a-zA-Z0-9_$]+)\s*:\s*([^=]+)\s*=\s*(async\s+)?function/g,
// export const name = async? function(params): ReturnType
/export\s+const\s+([a-zA-Z0-9_$]+)\s*=\s*(async\s+)?function\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?/g,
// export const name = async? (params) => (without explicit return type)
/export\s+const\s+([a-zA-Z0-9_$]+)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>/g,
];
for (const pattern of functionPatterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
let name;
let isAsync;
let params;
let returnType;
let signature;
// Handle different regex capture groups based on pattern
if (pattern.source.includes('export\\s+(async\\s+)?function')) {
// Pattern 1: export [async] function name<T>(params): ReturnType
isAsync = !!match[1];
name = match[2];
const generics = match[3] || '';
params = match[4] || '';
returnType = match[5] ? match[5].trim() : 'any';
signature = `function ${name}${generics}(${params})`;
if (isAsync) {
returnType = returnType === 'any' ? 'Promise<any>' : `Promise<${returnType}>`;
}
signature += `: ${returnType}`;
}
else if (pattern.source.includes('\\)\\s*:\\s*([^=>{]+)\\s*=>')) {
// Pattern 2: export const name = async? (params): ReturnType =>
name = match[1];
isAsync = !!match[2];
params = match[3] || '';
returnType = match[4] ? match[4].trim() : 'any';
signature = `const ${name}: (${params}) => ${returnType}`;
}
else if (pattern.source.includes(':\\s*([^=]+)\\s*=\\s*(async\\s+)?function')) {
// Pattern 3: export const name: Type = async? function
name = match[1];
const typeAnnotation = match[2] ? match[2].trim() : '';
isAsync = !!match[3];
signature = `const ${name}: ${typeAnnotation}`;
returnType = typeAnnotation;
params = '';
}
else if (pattern.source.includes('function\\s*\\(([^)]*)\\)(?:\\s*:\\s*([^{]+))?')) {
// Pattern 4: export const name = async? function(params): ReturnType
name = match[1];
isAsync = !!match[2];
params = match[3] || '';
returnType = match[4] ? match[4].trim() : 'any';
signature = `const ${name}: (${params}) => ${isAsync ? `Promise<${returnType}>` : returnType}`;
}
else {
// Pattern 5: export const name = async? (params) => (no explicit return type)
name = match[1];
isAsync = !!match[2];
params = match[3] || '';
returnType = isAsync ? 'Promise<any>' : 'any';
signature = `const ${name}: (${params}) => ${returnType}`;
}
// Don't add duplicates
if (!exportedItems.some(item => item.name === name)) {
exportedItems.push({
name,
type: 'function',
signature,
isAsync,
returnType,
params
});
}
}
}
}
// Class patterns
if (includeTypes.classes) {
const classPattern = /export\s+(?:abstract\s+)?class\s+([a-zA-Z0-9_$]+)(\s*<[^>]*>)?(?:\s+extends\s+([^{]+?))?(?:\s+implements\s+([^{]+?))?/g;
let match;
while ((match = classPattern.exec(content)) !== null) {
const className = match[1];
const generics = match[2] || '';
const extendsClause = match[3] ? match[3].trim() : '';
const implementsClause = match[4] ? match[4].trim() : '';
// Extract class body to find methods and properties
const classBodyRegex = new RegExp(`export\\s+(?:abstract\\s+)?class\\s+${className}[^{]*\\{([^}]*)\\}`, 's');
const classBodyMatch = classBodyRegex.exec(content);
let classInfo = {
methods: [],
properties: [],
constructor: undefined
};
if (classBodyMatch) {
const classBody = classBodyMatch[1];
// Extract constructor
const constructorMatch = classBody.match(/constructor\s*\(([^)]*)\)/);
if (constructorMatch) {
classInfo.constructor = { params: constructorMatch[1] };
}
// Extract methods
const methodRegex = /(static\s+)?(async\s+)?([a-zA-Z0-9_$]+)\s*(\<[^>]*\>)?\s*\(([^)]*)\)(?:\s*:\s*([^{;]+))?/g;
let methodMatch;
while ((methodMatch = methodRegex.exec(classBody)) !== null) {
const isStatic = !!methodMatch[1];
const isAsync = !!methodMatch[2];
const methodName = methodMatch[3];
const methodGenerics = methodMatch[4] || '';
const methodParams = methodMatch[5] || '';
const methodReturnType = methodMatch[6] ? methodMatch[6].trim() : 'any';
// Skip constructor (already handled)
if (methodName === 'constructor')
continue;
const methodSignature = `${isStatic ? 'static ' : ''}${isAsync ? 'async ' : ''}${methodName}${methodGenerics}(${methodParams}): ${methodReturnType}`;
classInfo.methods.push({
name: methodName,
signature: methodSignature,
isStatic
});
}
// Extract properties
const propertyRegex = /(static\s+)?(readonly\s+)?([a-zA-Z0-9_$]+)\s*:\s*([^;=]+)/g;
let propertyMatch;
while ((propertyMatch = propertyRegex.exec(classBody)) !== null) {
const isStatic = !!propertyMatch[1];
const isReadonly = !!propertyMatch[2];
const propertyName = propertyMatch[3];
const propertyType = propertyMatch[4].trim();
classInfo.properties.push({
name: propertyName,
type: `${isReadonly ? 'readonly ' : ''}${propertyType}`,
isStatic
});
}
}
// Build class signature
let signature = `class ${className}${generics}`;
if (extendsClause)
signature += ` extends ${extendsClause}`;
if (implementsClause)
signature += ` implements ${implementsClause}`;
exportedItems.push({
name: className,
type: 'class',
signature,
isAsync: false,
returnType: className,
params: classInfo.constructor?.params || '',
classInfo
});
}
}
// Constants and variables
if (includeTypes.constants || includeTypes.variables) {
const constantPattern = /export\s+const\s+([a-zA-Z0-9_$]+)\s*:\s*([^=]+)\s*=/g;
let match;
while ((match = constantPattern.exec(content)) !== null) {
const name = match[1];
const type = match[2].trim();
// Skip if it's already identified as a function
if (!exportedItems.some(item => item.name === name)) {
exportedItems.push({
name,
type: 'const',
signature: `const ${name}: ${type}`,
isAsync: false,
returnType: type,
params: ''
});
}
}
}
// Handle export { ... } statements
const exportStatements = content.match(/export\s*\{\s*([^}]+)\s*\}/g);
if (exportStatements) {
for (const statement of exportStatements) {
const match = statement.match(/export\s*\{\s*([^}]+)\s*\}/);
if (match) {
const exports = match[1].split(',').map(item => {
const cleaned = item.trim().split(/\s+as\s+/)[0];
return cleaned.trim();
});
for (const exportName of exports) {
if (!exportedItems.some(item => item.name === exportName)) {
// Try to determine the type by looking for declarations
if (includeTypes.classes && new RegExp(`(?:abstract\\s+)?class\\s+${exportName}\\s*[<{]`).test(content)) {
exportedItems.push({
name: exportName,
type: 'class',
signature: `class ${exportName}`,
isAsync: false,
returnType: exportName,
params: '',
classInfo: { methods: [], properties: [], constructor: undefined }
});
}
else if (includeTypes.functions && new RegExp(`(?:function\\s+${exportName}|const\\s+${exportName}\\s*=\\s*(?:async\\s+)?(?:function|\\())`).test(content)) {
exportedItems.push({
name: exportName,
type: 'function',
signature: `function ${exportName}(...args: any[]): any`,
isAsync: false,
returnType: 'any',
params: '...args: any[]'
});
}
}
}
}
}
}
return exportedItems;
}
// Helper function to load SvelteKit environment variables
async function loadSvelteKitEnvironment(context, debugLevel) {
const envFiles = [
'.env',
'.env.local',
'.env.development',
'.env.development.local'
];
let totalEnvVarsLoaded = 0;
for (const envFile of envFiles) {
const envPath = path.resolve(context.workingDir, envFile);
if (fs.existsSync(envPath)) {
try {
const envContent = fs.readFileSync(envPath, 'utf-8');
const envVars = parseEnvFile(envContent);
if (debugLevel === "verbose") {
context.logger.log(`🔍 Found ${Object.keys(envVars).length} variables in ${envFile}:`);
}
// Set environment variables
for (const [key, value] of Object.entries(envVars)) {
if (!process.env[key]) {
process.env[key] = value;
context.logger.log(` ✅ ${key} = ${value.substring(0, 20)}${value.length > 20 ? '...' : ''}`);
totalEnvVarsLoaded++;
}
else {
context.logger.log(` ⚠️ ${key} already set, skipping`);
}
}
if (debugLevel !== "off") {
context.logger.log(`🌍 Loaded ${Object.keys(envVars).length} environment variables from ${envFile}`);
}
}
catch (error) {
if (debugLevel !== "off") {
context.logger.warn(`⚠️ Could not load ${envFile}:`, error);
}
}
}
else if (debugLevel === "verbose") {
context.logger.log(`📝 ${envFile} not found`);
}
}
// Debug: Show specific variables we care about
if (debugLevel === "verbose") {
const importantVars = ['PUBLIC_SUPABASE_URL', 'SUPABASE_SERVICE_KEY'];
context.logger.log(`🔍 Checking important environment variables:`);
for (const varName of importantVars) {
const value = process.env[varName];
if (value) {
context.logger.log(` ✅ ${varName} = ${value.substring(0, 20)}${value.length > 20 ? '...' : ''}`);
}
else {
context.logger.log(` ❌ ${varName} = undefined`);
}
}
context.logger.log(`📊 Total environment variables loaded: ${totalEnvVarsLoaded}`);
}
}
// Helper function to parse .env file content
function parseEnvFile(content) {
const envVars = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
// Skip empty lines and comments
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
// Parse KEY=VALUE
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
if (match) {
const [, key, value] = match;
// Remove quotes if present
const cleanValue = value.replace(/^["']|["']$/g, '');
envVars[key] = cleanValue;
}
}
return envVars;
}
// Helper function to scan a folder recursively for exports
async function scanFolderForExports(folderPath, filePatterns, excludePatterns, fileCache, context, includeTypes) {
try {
const files = await fs.promises.readdir(folderPath, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(folderPath, file.name);
// Skip entries that match exclude patterns
if (excludePatterns.some(pattern => file.name.includes(pattern))) {
continue;
}
if (file.isDirectory()) {
// Recursively scan subdirectories
await scanFolderForExports(fullPath, filePatterns, excludePatterns, fileCache, context, includeTypes);
}
else if (file.isFile()) {
// Check if the file matches our patterns
const hasMatchingExtension = filePatterns.some(ext => file.name.endsWith(ext));
if (hasMatchingExtension) {
// Get file stats to check modification time
const fileStats = fs.statSync(fullPath);
const currentLastModified = fileStats.mtimeMs;
const existingCache = fileCache.get(fullPath);
// Check if the file has been modified since last build
if (!existingCache || currentLastModified !== existingCache.lastModified) {
context.logger.log(`Found or updated export file: ${fullPath}`);
try {
const content = fs.readFileSync(fullPath, 'utf-8');
// Transform SvelteKit environment imports
const transformedContent = transformSvelteKitEnvImports(content);
// Only cache if it has exports we care about
const exportedItems = extractExportedItems(transformedContent, includeTypes);
if (exportedItems.length > 0) {
const exportTypes = exportedItems.map(item => item.type);
const typeCounts = exportTypes.reduce((acc, type) => {
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {});
const typeCountStr = Object.entries(typeCounts)
.map(([type, count]) => `${count} ${type}${count > 1 ? 's' : ''}`)
.join(', ');
console.log(`✅ Found ${exportedItems.length} exports in ${file.name}: ${typeCountStr}`);
fileCache.set(fullPath, {
path: fullPath,
lastModified: currentLastModified,
content: transformedContent
});
}
else {
console.log(`❌ No matching exports found in ${file.name}`);
}
}
catch (error) {
context.logger.warn(`Error reading file ${fullPath}:`, error);
}
}
else {
context.logger.log(`Using cached version of: ${fullPath}`);
}
}
}
}
}
catch (error) {
context.logger.warn(`Error scanning folder ${folderPath}:`, error);
}
}
// Helper function to scan a folder for environment variables
async function scanFolderForEnvVars(dirPath, filePatterns, excludePatterns, envVars, context, debugLevel) {
try {
const files = await fs.promises.readdir(dirPath, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dirPath, file.name);
// Skip excluded files
if (excludePatterns.some(pattern => file.name.includes(pattern)))
continue;
if (file.isDirectory()) {
// Recursively scan subdirectories
await scanFolderForEnvVars(fullPath, filePatterns, excludePatterns, envVars, context, debugLevel);
}
else if (file.isFile() && filePatterns.some(ext => file.name.endsWith(ext))) {
try {
// Read file content
const content = fs.readFileSync(fullPath, 'utf8');
// Extract env vars
extractEnvironmentVariables(content, envVars);
}
catch (error) {
if (debugLevel !== "off") {
context.logger.warn(`Error reading file: ${fullPath}`, error);
}
}
}
}
}
catch (error) {
if (debugLevel !== "off") {
context.logger.warn(`Error scanning dir for env vars: ${dirPath}`, error);
}
}
}
// Helper function to transform SvelteKit env imports to use process.env
function transformSvelteKitEnvImports(source) {
// Transform imports from $env/static/public or $env/static/private
return source.replace(/import\s+\{\s*([^}]+)\s*\}\s+from\s+['"](\$env\/static\/(?:public|private))['"]/g, (match, imports) => {
// Parse the imported variables
const variables = imports.split(',').map(v => v.trim());
// Generate a destructuring assignment from process.env
const result = `const { ${variables.join(', ')} } = process.env;\n`;
return result;
});
}
// Enhanced function to extract function signatures with full type information
function extractFunctionSignatures(content) {
const functionSignatures = [];
// Enhanced regex patterns to capture full function signatures
const patterns = [
// export function name<T>(params): ReturnType or export async function name<T>(params): ReturnType
/export\s+(async\s+)?function\s+([a-zA-Z0-9_$]+)(\s*<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^{;]+))?/g,
// export const name = (params): ReturnType => or export const name = async (params): ReturnType =>
/export\s+const\s+([a-zA-Z0-9_$]+)\s*=\s*(async\s+)?\(([^)]*)\)\s*:\s*([^=>{]+)\s*=>/g,
// export const name: Type = function or export const name: Type = async function
/export\s+const\s+([a-zA-Z0-9_$]+)\s*:\s*([^=]+)\s*=\s*(async\s+)?function/g,
// export const name = async? function(params): ReturnType
/export\s+const\s+([a-zA-Z0-9_$]+)\s*=\s*(async\s+)?function\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?/g,
// export const name = async? (params) => (without explicit return type)
/export\s+const\s+([a-zA-Z0-9_$]+)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>/g,
];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
let name;
let isAsync;
let params;
let returnType;
let signature;
// Handle different regex capture groups based on pattern
if (pattern.source.includes('export\\s+(async\\s+)?function')) {
// Pattern 1: export [async] function name<T>(params): ReturnType
isAsync = !!match[1];
name = match[2];
const generics = match[3] || '';
params = match[4] || '';
returnType = match[5] ? match[5].trim() : 'any';
signature = `function ${name}${generics}(${params})`;
if (isAsync) {
returnType = returnType === 'any' ? 'Promise<any>' : `Promise<${returnType}>`;
}
signature += `: ${returnType}`;
}
else if (pattern.source.includes('\\)\\s*:\\s*([^=>{]+)\\s*=>')) {
// Pattern 2: export const name = async? (params): ReturnType =>
name = match[1];
isAsync = !!match[2];
params = match[3] || '';
returnType = match[4] ? match[4].trim() : 'any';
signature = `const ${name}: (${params}) => ${returnType}`;
}
else if (pattern.source.includes(':\\s*([^=]+)\\s*=\\s*(async\\s+)?function')) {
// Pattern 3: export const name: Type = async? function
name = match[1];
const typeAnnotation = match[2] ? match[2].trim() : '';
isAsync = !!match[3];
signature = `const ${name}: ${typeAnnotation}`;
returnType = typeAnnotation;
params = '';
}
else if (pattern.source.includes('function\\s*\\(([^)]*)\\)(?:\\s*:\\s*([^{]+))?')) {
// Pattern 4: export const name = async? function(params): ReturnType
name = match[1];
isAsync = !!match[2];
params = match[3] || '';
returnType = match[4] ? match[4].trim() : 'any';
signature = `const ${name}: (${params}) => ${isAsync ? `Promise<${returnType}>` : returnType}`;
}
else {
// Pattern 5: export const name = async? (params) => (no explicit return type)
name = match[1];
isAsync = !!match[2];
params = match[3] || '';
returnType = isAsync ? 'Promise<any>' : 'any';
signature = `const ${name}: (${params}) => ${returnType}`;
}
// Don't add duplicates
if (!functionSignatures.some(f => f.name === name)) {
functionSignatures.push({
name,
signature,
isAsync,
returnType,
params
});
}
}
}
// Handle export { ... } statements by looking for function declarations
const exportStatements = content.match(/export\s*\{\s*([^}]+)\s*\}/g);
if (exportStatements) {
for (const statement of exportStatements) {
const match = statement.match(/export\s*\{\s*([^}]+)\s*\}/);
if (match) {
const exports = match[1].split(',').map(item => {
const cleaned = item.trim().split(/\s+as\s+/)[0];
return cleaned.trim();
});
for (const exportName of exports) {
// Look for the function declaration in the content
const patterns = [
new RegExp(`(async\\s+)?function\\s+${exportName}\\s*(\\<[^>]*\\>)?\\s*\\(([^)]*)\\)(?:\\s*:\\s*([^{;]+))?`, 'g'),
new RegExp(`const\\s+${exportName}\\s*=\\s*(async\\s+)?\\(([^)]*)\\)\\s*:\\s*([^=>{]+)\\s*=>`, 'g'),
new RegExp(`const\\s+${exportName}\\s*=\\s*(async\\s+)?\\(([^)]*)\\)\\s*=>`, 'g'),
new RegExp(`const\\s+${exportName}\\s*:\\s*([^=]+)\\s*=`, 'g'),
];
for (const funcPattern of patterns) {
const funcMatch = funcPattern.exec(content);
if (funcMatch && !functionSignatures.some(f => f.name === exportName)) {
let isAsync;
let params;
let returnType;
let signature;
if (funcPattern.source.includes('function')) {
isAsync = !!funcMatch[1];
const generics = funcMatch[2] || '';
params = funcMatch[3] || '';
returnType = funcMatch[4] ? funcMatch[4].trim() : 'any';
signature = `function ${exportName}${generics}(${params})`;
if (isAsync) {
returnType = returnType === 'any' ? 'Promise<any>' : `Promise<${returnType}>`;
}
signature += `: ${returnType}`;
}
else if (funcPattern.source.includes('\\)\\s*:\\s*([^=>{]+)')) {
isAsync = !!funcMatch[1];
params = funcMatch[2] || '';
returnType = funcMatch[3] ? funcMatch[3].trim() : 'any';
signature = `const ${exportName}: (${params}) => ${returnType}`;
}
else if (funcPattern.source.includes('\\)\\s*=>')) {
isAsync = !!funcMatch[1];
params = funcMatch[2] || '';
returnType = isAsync ? 'Promise<any>' : 'any';
signature = `const ${exportName}: (${params}) => ${returnType}`;
}
else {
const typeAnnotation = funcMatch[1] ? funcMatch[1].trim() : 'any';
signature = `const ${exportName}: ${typeAnnotation}`;
returnType = typeAnnotation;
params = '';
isAsync = typeAnnotation.includes('Promise');
}
functionSignatures.push({
name: exportName,
signature,
isAsync,
returnType,
params
});
break;
}
}
}
}
}
}
return functionSignatures;
}
// Helper function to extract environment variables from a file
function extractEnvironmentVariables(source, envVars) {
// Find all imports from $env/static/public or $env/static/private
const envImportRegex = /import\s+\{\s*([^}]+)\s*\}\s+from\s+['"](\$env\/static\/(?:public|private))['"]/g;
let match;
// Process all env imports (both public and private)
while ((match = envImportRegex.exec(source)) !== null) {
const variables = match[1].split(',').map(v => v.trim());
for (const varName of variables) {
envVars.add(varName);
}
}
}
// Generate virtual module content based on export strategy
function generateVirtualModuleContent(fileCache, envVars, options, context) {
let moduleContent = '// Generated by triggerkit\n\n';
// Environment variables
if (envVars.size > 0) {
moduleContent += '// Environment variables\n';
context.logger.log(`🌍 Generating exports for ${envVars.size} environment variables:`);
for (const varName of envVars) {
const envValue = process.env[varName];
context.logger.log(` 📝 ${varName} = ${envValue ? envValue.substring(0, 20) + (envValue.length > 20 ? '...' : '') : 'undefined'}`);
moduleContent += `export const ${varName} = process.env.${varName};\n`;
}
moduleContent += '\n';
}
// Organize files by folder and extract exports
const filesByFolder = {};
for (const [filePath, cachedFile] of fileCache.entries()) {
if (!cachedFile.content)
continue;
const exportedItems = extractExportedItems(cachedFile.content, options.includeTypes);
if (exportedItems.length === 0)
continue;
const relPath = path.relative(context.workingDir, filePath);
const dirName = path.dirname(relPath);
if (!filesByFolder[dirName]) {
filesByFolder[dirName] = new Map();
}
filesByFolder[dirName].set(filePath, {
path: filePath,
relPath,
exportedItems
});
}
const exportStrategy = options.exportStrategy;
if (exportStrategy.mode === "individual") {
return generateIndividualExports(filesByFolder, moduleContent);
}
else if (exportStrategy.mode === "grouped") {
return generateGroupedExports(filesByFolder, moduleContent, exportStrategy);
}
else if (exportStrategy.mode === "mixed") {
return generateMixedExports(filesByFolder, moduleContent, exportStrategy);
}
return moduleContent;
}
function generateGroupedExports(filesByFolder, moduleContent, strategy) {
const groups = {};
const imports = [];
// Organize exports into groups
for (const [folder, files] of Object.entries(filesByFolder)) {
for (const [_, fileInfo] of files.entries()) {
const importPath = fileInfo.relPath.replace(/\.(ts|js)$/, '');
const importNames = fileInfo.exportedItems.map(item => item.name).join(', ');
imports.push(`import { ${importNames} } from '${importPath}';`);
for (const item of fileInfo.exportedItems) {
let groupName;
switch (strategy.groupBy) {
case "file":
const fileName = fileInfo.relPath.split('/').pop()?.replace(/\.(ts|js)$/, '') || 'default';
groupName = strategy.groupPrefix ? `${strategy.groupPrefix}_${fileName}` : fileName;
break;
case "folder":
const folderName = folder.replace(/[\/\\]/g, '_');
groupName = strategy.groupPrefix ? `${strategy.groupPrefix}_${folderName}` : folderName;
break;
default:
groupName = folder.replace(/[\/\\]/g, '_');
}
// Sanitize group name
groupName = groupName.replace(/[^a-zA-Z0-9_]/g, '_');
if (!groups[groupName]) {
groups[groupName] = [];
}
groups[groupName].push(item.name);
}
}
}
// Add imports
moduleContent += '// Imports\n';
moduleContent += imports.join('\n') + '\n\n';
// Export groups
moduleContent += '// Grouped exports\n';
for (const [groupName, exportNames] of Object.entries(groups)) {
moduleContent += `export const ${groupName} = {\n`;
for (const exportName of exportNames) {
moduleContent += ` ${exportName},\n`;
}
moduleContent += '};\n\n';
}
// Export individual items for backwards compatibility
moduleContent += '// Individual exports (for backwards compatibility)\n';
const allExports = Object.values(groups).flat();
moduleContent += `export { ${allExports.join(', ')} };\n\n`;
// Export functions object
const allFunctions = allExports.filter(name => {
// Find the item to check its type
for (const files of Object.values(filesByFolder)) {
for (const fileInfo of files.values()) {
const item = fileInfo.exportedItems.find(item => item.name === name);
if (item)
return item.type === 'function';
}
}
return false;
});
if (allFunctions.length > 0) {
moduleContent += '// Functions object with all function exports\n';
moduleContent += 'export const functions = {\n';
for (const funcName of allFunctions) {
moduleContent += ` ${funcName},\n`;
}
moduleContent += '};\n';
}
return moduleContent;
}
function generateMixedExports(filesByFolder, moduleContent, strategy) {
// Generate both individual and grouped exports
const individualContent = generateIndividualExports(filesByFolder, '');
const groupedContent = generateGroupedExports(filesByFolder, '', strategy);
return moduleContent + individualContent + '\n// Additional grouped exports\n' + groupedContent.split('// Imports\n')[1];
}
function generateIndividualExports(filesByFolder, moduleContent) {
// Import and export all items individually
for (const [folder, files] of Object.entries(filesByFolder)) {
moduleContent += `// Exports from ${folder}\n`;
for (const [_, fileInfo] of files.entries()) {
if (fileInfo.exportedItems.length === 0)
continue;
const importPath = fileInfo.relPath.replace(/\.(ts|js)$/, '');
const importNames = fileInfo.exportedItems.map(item => item.name).join(', ');
moduleContent += `import { ${importNames} } from '${importPath}';\n`;
}
const allExportsInFolder = [];
for (const fileInfo of files.values()) {
allExportsInFolder.push(...fileInfo.exportedItems.map(item => item.name));
}
if (allExportsInFolder.length > 0) {
moduleContent += `export { ${allExportsInFolder.join(', ')} };\n\n`;
}
}
// Add functions object
const allFunctions = [];
for (const files of Object.values(filesByFolder)) {
for (const fileInfo of files.values()) {
allFunctions.push(...fileInfo.exportedItems.filter(item => item.type === 'function').map(item => item.name));
}
}
if (allFunctions.length > 0) {
moduleContent += '// Functions object with all function exports\n';
moduleContent += 'export const functions = {\n';
for (const funcName of allFunctions) {
moduleContent += ` ${funcName},\n`;
}
moduleContent += '};\n';
}
return moduleContent;
}
// Enhanced type declaration generation
function generateFunctionTypeDeclarations(context, fileCache) {
try {
// Build the declaration content
let declarationContent = `// Generated by triggerkit - Enhanced with actual TypeScript types\n\n`;
declarationContent += `declare module '${VIRTUAL_MODULE_ID}' {\n`;
// Group files by folder for better organization
const filesByFolder = {};
// Organize files by folder
for (const [filePath, cachedFile] of fileCache.entries()) {
if (!cachedFile.content)
continue;
// Get the relative path from the working directory
const relPath = path.relative(context.workingDir, filePath);
// Get the directory for grouping
const dirName = path.dirname(relPath);
if (!filesByFolder[dirName]) {
filesByFolder[dirName] = [];
}
filesByFolder[dirName].push(filePath);
}
// Track all function names for the functions object
const allFunctionNames = [];
// Process each folder
for (const [folder, filePaths] of Object.entries(filesByFolder)) {
declarationContent += ` // Functions from ${folder}\n`;
// Process each file in the folder
for (const filePath of filePaths) {
const cachedFile = fileCache.get(filePath);
if (!cachedFile || !cachedFile.content)
continue;
const content = cachedFile.content;
const fileName = path.basename(filePath);
// Extract function signatures with full type information
const functionSignatures = extractFunctionSignatures(content);
if (functionSignatures.length > 0) {
declarationContent += ` // From ${fileName}\n`;
for (const func of functionSignatures) {
// Generate proper export declaration based on signature type
if (func.signature.startsWith('function')) {
declarationContent += ` export ${func.signature};\n`;
}
else if (func.signature.startsWith('const')) {
// Convert const signature to export declaration
const funcDeclaration = func.signature.replace(/^const\s+(\w+):\s*/, 'export declare const $1: ');
declarationContent += ` ${funcDeclaration};\n`;
}
else {
// Fallback to basic function declaration
declarationContent += ` export declare function ${func.name}(${func.params}): ${func.returnType};\n`;
}
allFunctionNames.push(func.name);
}
declarationContent += '\n';
}
}
}
// Generate environment variables if any
const envVars = new Set();
for (const [_, cachedFile] of fileCache.entries()) {
if (cachedFile.content) {
extractEnvironmentVariables(cachedFile.content, envVars);
}
}
if (envVars.size > 0) {
declarationContent += ' // Environment variables\n';
for (const varName of envVars) {
declarationContent += ` export declare const ${varName}: string;\n`;
}
declarationContent += '\n';
}
// Add the functions object with proper typing
if (allFunctionNames.length > 0) {
declarationContent += ' // Functions object with all exports\n';
declarationContent += ' export declare const functions: {\n';
for (const funcName of allFunctionNames) {
declarationContent += ` ${funcName}: typeof ${funcName};\n`;
}
declarationContent += ' };\n';
}
// Close the module declaration
declarationContent += `}\n`;
// Write to the src directory
try {
const srcDeclPath = path.resolve(process.cwd(), `src/virtual-triggerkit.d.ts`);
fs.writeFileSync(srcDeclPath, declarationContent);
context.logger.log(`📝 Generated enhanced TypeScript declaration file at: ${srcDeclPath}`);
context.logger.log(`🎯 Included ${allFunctionNames.length} functions with full type information`);
}
catch (error) {
context.logger.warn(`Error writing declaration file:`, error);
}
}
catch (error) {
context.logger.warn(`Error generating TypeScript declaration file:`, error);
}
}
export { triggerkit, VIRTUAL_MODULE_ID };