@nlabs/lex
Version:
810 lines (809 loc) • 117 kB
JavaScript
/**
* Copyright (c) 2018-Present, Nitrogen Labs, Inc.
* Copyrights licensed under the MIT License. See the accompanying LICENSE file for terms.
*/ import boxen from 'boxen';
import chalk from 'chalk';
import express from 'express';
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
import { homedir } from 'os';
import { resolve as pathResolve, join, isAbsolute } from 'path';
import { pathToFileURL } from 'url';
import { WebSocketServer } from 'ws';
import { LexConfig, getPackageDir } from '../../LexConfig.js';
import { createSpinner, removeFiles } from '../../utils/app.js';
import { log } from '../../utils/log.js';
const getCacheDir = ()=>{
const cacheDir = join(homedir(), '.lex-cache');
if (!existsSync(cacheDir)) {
mkdirSync(cacheDir, {
recursive: true
});
}
return cacheDir;
};
const getCachePath = ()=>join(getCacheDir(), 'public-ip.json');
const readPublicIpCache = ()=>{
const cachePath = getCachePath();
if (!existsSync(cachePath)) {
return null;
}
try {
const cacheData = readFileSync(cachePath, 'utf8');
const cache = JSON.parse(cacheData);
// Check if cache is older than 1 week
const oneWeekMs = 7 * 24 * 60 * 60 * 1000;
if (Date.now() - cache.timestamp > oneWeekMs) {
return null;
}
return cache;
} catch {
return null;
}
};
const writePublicIpCache = (ip)=>{
const cachePath = getCachePath();
const cache = {
ip,
timestamp: Date.now()
};
writeFileSync(cachePath, JSON.stringify(cache, null, 2));
};
const fetchPublicIp = (forceRefresh = false)=>new Promise((resolve)=>{
if (!forceRefresh) {
const cached = readPublicIpCache();
if (cached) {
resolve(cached.ip);
return;
}
}
// Use fetch instead of https
fetch('https://api.ipify.org').then((res)=>res.text()).then((data)=>{
const ip = data.trim();
if (ip) {
writePublicIpCache(ip);
}
resolve(ip);
}).catch(()=>resolve(undefined));
});
const displayServerStatus = (httpPort, httpsPort, wsPort, host, quiet, publicIp)=>{
if (quiet) {
return;
}
const httpUrl = `http://${host}:${httpPort}`;
const httpsUrl = `https://${host}:${httpsPort}`;
const wsUrl = `ws://${host}:${wsPort}`;
const wssUrl = `wss://${host}:${wsPort}`;
let urlLines = `${chalk.green('HTTP:')} ${chalk.underline(httpUrl)}\n`;
urlLines += `${chalk.green('HTTPS:')} ${chalk.underline(httpsUrl)}\n`;
urlLines += `${chalk.green('WebSocket:')} ${chalk.underline(wsUrl)}\n`;
urlLines += `${chalk.green('WSS:')} ${chalk.underline(wssUrl)}\n`;
if (publicIp) {
urlLines += `\n${chalk.green('Public:')} ${chalk.underline(`http://${publicIp}:${httpPort}`)}\n`;
}
const statusBox = boxen(`${chalk.cyan.bold('🚀 Serverless Development Server Running')}\n\n${urlLines}\n` + `${chalk.yellow('Press Ctrl+C to stop the server')}`, {
backgroundColor: '#1a1a1a',
borderColor: 'cyan',
borderStyle: 'round',
margin: 1,
padding: 1
});
console.log(`\n${statusBox}\n`);
};
const loadHandler = async (handlerPath, outputDir)=>{
try {
console.log(`[Serverless] Parsing handler path: ${handlerPath}`);
// Parse AWS Lambda handler format: "path/to/file.exportName" or "file.exportName"
// Examples: "index.handler", "handlers/api.handler", "src/index.default"
const handlerParts = handlerPath.split('.');
console.log('[Serverless] Handler parts after split:', handlerParts);
let filePath;
let exportName = null;
if (handlerParts.length > 1) {
// AWS Lambda format: "file.exportName"
// Take the last part as export name, rest as file path
// Handle cases like "index.handler" -> file: "index", export: "handler"
// Or "handlers/api.handler" -> file: "handlers/api", export: "handler"
exportName = handlerParts[handlerParts.length - 1] || null;
filePath = handlerParts.slice(0, -1).join('.');
console.log(`[Serverless] Parsed AWS Lambda format - filePath: "${filePath}", exportName: "${exportName}"`);
} else {
// Simple format: just the file path
filePath = handlerPath;
console.log(`[Serverless] Simple format - filePath: "${filePath}"`);
}
// Ensure filePath doesn't have the export name in it
if (filePath.includes('.handler') || filePath.includes('.default')) {
console.error(`[Serverless] WARNING: filePath still contains export name! filePath: "${filePath}"`);
// Try to fix it - remove the last part if it looks like an export name
const pathParts = filePath.split('.');
if (pathParts.length > 1) {
const lastPart = pathParts[pathParts.length - 1];
if ([
'handler',
'default',
'index'
].includes(lastPart) && !exportName) {
exportName = lastPart;
filePath = pathParts.slice(0, -1).join('.');
console.log(`[Serverless] Fixed - filePath: "${filePath}", exportName: "${exportName}"`);
}
}
}
// Handle both relative paths and absolute paths
let fullPath;
if (isAbsolute(filePath)) {
fullPath = filePath;
} else {
fullPath = pathResolve(outputDir, filePath);
}
console.log(`[Serverless] Resolved fullPath (before extensions): ${fullPath}`);
// Try different extensions if file doesn't exist
if (!existsSync(fullPath)) {
const extensions = [
'.js',
'.mjs',
'.cjs'
];
const pathWithoutExt = fullPath.replace(/\.(js|mjs|cjs)$/, '');
console.log(`[Serverless] Trying extensions. Base path: ${pathWithoutExt}`);
for (const ext of extensions){
const candidatePath = pathWithoutExt + ext;
console.log(`[Serverless] Checking: ${candidatePath} (exists: ${existsSync(candidatePath)})`);
if (existsSync(candidatePath)) {
fullPath = candidatePath;
console.log(`[Serverless] Found file with extension: ${fullPath}`);
break;
}
}
} else {
console.log(`[Serverless] File exists without trying extensions: ${fullPath}`);
}
console.log(`[Serverless] Final fullPath: ${fullPath}`);
console.log(`[Serverless] Export name: ${exportName || 'default/handler'}`);
console.log(`[Serverless] File exists: ${existsSync(fullPath)}`);
if (!existsSync(fullPath)) {
console.error(`[Serverless] Handler file not found: ${fullPath}`);
console.error(`[Serverless] Output directory: ${outputDir}`);
console.error(`[Serverless] Handler path from config: ${handlerPath}`);
throw new Error(`Handler file not found: ${fullPath}`);
}
// Dynamic import of the handler with better error handling
// Add .js extension if importing TypeScript compiled output
const importPath = fullPath.endsWith('.ts') ? fullPath.replace(/\.ts$/, '.js') : fullPath;
try {
// Convert to file:// URL for ES module imports (required for absolute paths)
// Use pathToFileURL to ensure proper file:// URL format
const importUrl = pathToFileURL(importPath).href;
console.log(`[Serverless] Importing handler from: ${importUrl}`);
console.log(`[Serverless] File path: ${importPath}`);
// Use import() with the file URL
// Note: If the handler file has import errors (like missing dependencies),
// those will surface here, but that's a handler code issue, not a loader issue
const handlerModule = await import(importUrl);
console.log(`[Serverless] Handler module loaded successfully. Exports: ${Object.keys(handlerModule).join(', ')}`);
// Get the handler based on export name or try defaults
let handler;
if (exportName) {
handler = handlerModule[exportName];
if (!handler) {
console.error(`[Serverless] Export "${exportName}" not found in module. Available exports: ${Object.keys(handlerModule).join(', ')}`);
return null;
}
} else {
// Try default, handler, or the module itself
handler = handlerModule.default || handlerModule.handler || handlerModule;
}
console.log(`[Serverless] Handler found: ${typeof handler}, isFunction: ${typeof handler === 'function'}`);
if (typeof handler !== 'function') {
console.error(`[Serverless] Handler is not a function. Type: ${typeof handler}, Value:`, handler);
return null;
}
return handler;
} catch (importError) {
console.error(`[Serverless] Import error for handler ${handlerPath}:`, importError.message);
console.error('[Serverless] Import error stack:', importError.stack);
// Check if this is a dependency resolution error (common with ES modules)
if (importError.message && importError.message.includes('Cannot find module')) {
console.error('[Serverless] This appears to be a dependency resolution error.');
console.error('[Serverless] The handler file exists, but one of its imports is failing.');
console.error('[Serverless] Check that all dependencies in the handler file are properly installed.');
console.error(`[Serverless] Handler file: ${importPath}`);
console.error('[Serverless] Make sure the handler and its dependencies are compiled correctly.');
}
return null;
}
} catch (error) {
console.error(`[Serverless] Error loading handler ${handlerPath}:`, error.message);
console.error('[Serverless] Error stack:', error.stack);
return null;
}
};
const captureConsoleLogs = (handler, quiet)=>{
if (quiet) {
return handler;
}
return async (event, context)=>{
// Capture console.log, console.error, etc.
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
const originalConsoleInfo = console.info;
const logs = [];
console.log = (...args)=>{
logs.push(`[LOG] ${args.join(' ')}`);
originalConsoleLog(...args);
};
console.error = (...args)=>{
logs.push(`[ERROR] ${args.join(' ')}`);
originalConsoleError(...args);
};
console.warn = (...args)=>{
logs.push(`[WARN] ${args.join(' ')}`);
originalConsoleWarn(...args);
};
console.info = (...args)=>{
logs.push(`[INFO] ${args.join(' ')}`);
originalConsoleInfo(...args);
};
try {
const result = await handler(event, context);
// Output captured logs
if (logs.length > 0) {
console.log(chalk.gray('--- Handler Console Output ---'));
logs.forEach((log)=>console.log(chalk.gray(log)));
console.log(chalk.gray('--- End Handler Console Output ---'));
}
return result;
} finally{
// Restore original console methods
console.log = originalConsoleLog;
console.error = originalConsoleError;
console.warn = originalConsoleWarn;
console.info = originalConsoleInfo;
}
};
};
const createExpressServer = async (config, outputDir, httpPort, host, quiet, debug)=>{
const app = express();
// Enable CORS
app.use((req, res, next)=>{
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
res.header('Access-Control-Allow-Headers', '*');
res.header('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
// Parse JSON bodies
app.use(express.json());
// Load GraphQL handler
const loadGraphQLSchema = async ()=>{
try {
// Try to find a GraphQL handler
let graphqlHandler = null;
if (config.functions) {
for (const [functionName, functionConfig] of Object.entries(config.functions)){
if (functionConfig.events) {
for (const event of functionConfig.events){
if (event.http && event.http.path) {
// Look for GraphQL endpoints
if (event.http.path === '/public' || event.http.path === '/graphql') {
graphqlHandler = await loadHandler(functionConfig.handler, outputDir);
break;
}
}
}
}
if (graphqlHandler) {
break;
}
}
}
if (graphqlHandler) {
log('Found GraphQL handler', 'info', quiet);
return graphqlHandler;
}
return null;
} catch (error) {
log(`Error loading GraphQL handler: ${error.message}`, 'error', quiet);
return null;
}
};
// Set up GraphQL handler for GraphQL requests
try {
const graphqlHandler = await loadGraphQLSchema();
if (graphqlHandler) {
// Find the GraphQL path from the serverless config
let graphqlPath = '/graphql'; // default fallback
if (config.functions) {
for (const [_functionName, functionConfig] of Object.entries(config.functions)){
if (functionConfig.events) {
for (const event of functionConfig.events){
if (event?.http?.path) {
graphqlPath = event.http.path;
break;
}
}
}
if (graphqlPath !== '/graphql') {
break;
}
}
}
// Set up GraphQL endpoint with enhanced console.log capture
app.use(graphqlPath, async (req, res)=>{
// GraphQL Debug Logging
if (debug && req.body && req.body.query) {
log('🔍 GraphQL Debug Mode: Analyzing request...', 'info', false);
log(`📝 GraphQL Query: ${req.body.query}`, 'info', false);
if (req.body.variables) {
log(`📊 GraphQL Variables: ${JSON.stringify(req.body.variables, null, 2)}`, 'info', false);
}
if (req.body.operationName) {
log(`🏷️ GraphQL Operation: ${req.body.operationName}`, 'info', false);
}
}
// Enhanced console.log capture
const originalConsoleLog = console.log;
const logs = [];
console.log = (...args)=>{
const logMessage = args.map((arg)=>typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)).join(' ');
logs.push(logMessage);
originalConsoleLog(`[GraphQL] ${logMessage}`);
};
// Create context for the handler
const context = {
awsRequestId: 'test-request-id',
functionName: 'graphql',
functionVersion: '$LATEST',
getRemainingTimeInMillis: ()=>30000,
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:graphql',
logGroupName: '/aws/lambda/graphql',
logStreamName: 'test-log-stream',
req,
res
};
// Wrap handler with console log capture
const wrappedHandler = captureConsoleLogs(graphqlHandler, quiet);
try {
// Call the handler with GraphQL parameters
const result = await wrappedHandler({
body: JSON.stringify(req.body),
headers: req.headers,
httpMethod: 'POST',
path: graphqlPath,
queryStringParameters: {}
}, context);
// Restore console.log
console.log = originalConsoleLog;
// Handle the result
if (result && typeof result === 'object' && result.statusCode) {
res.status(result.statusCode);
if (result.headers) {
Object.entries(result.headers).forEach(([key, value])=>{
res.setHeader(key, String(value));
});
}
res.send(result.body);
} else {
res.json(result);
}
} catch (error) {
// Restore console.log
console.log = originalConsoleLog;
log(`GraphQL handler error: ${error.message}`, 'error', false);
res.status(500).json({
error: error.message
});
}
});
log(`GraphQL endpoint available at http://${host}:${httpPort}${graphqlPath}`, 'info', quiet);
}
} catch (error) {
log(`Error setting up GraphQL: ${error.message}`, 'error', quiet);
}
// Fallback for non-GraphQL routes - handle all remaining routes
app.use('/', async (req, res)=>{
try {
const url = req.url || '/';
const method = req.method || 'GET';
const pathname = req.path || url.split('?')[0]; // Extract pathname without query string
// Always log requests (not affected by quiet flag for debugging)
console.log(`[Serverless] ${method} ${url} (pathname: ${pathname})`);
// Find matching function
let matchedFunction = null;
if (config.functions) {
const functionNames = Object.keys(config.functions);
console.log(`[Serverless] Available functions: ${functionNames.join(', ')}`);
console.log('[Serverless] Config functions:', JSON.stringify(config.functions, null, 2));
for (const [functionName, functionConfig] of Object.entries(config.functions)){
if (functionConfig.events) {
for (const event of functionConfig.events){
if (event.http) {
const eventPath = event.http.path || '/';
const eventMethod = (event.http.method || 'GET').toUpperCase();
const requestMethod = method.toUpperCase();
console.log(`[Serverless] Checking function ${functionName}: path="${eventPath}", method="${eventMethod}" against pathname="${pathname}", method="${requestMethod}"`);
// Improved path matching - compare pathname without query string
// Normalize paths (remove trailing slashes for comparison)
const normalizedEventPath = eventPath.replace(/\/$/, '') || '/';
const normalizedPathname = pathname.replace(/\/$/, '') || '/';
if (normalizedEventPath === normalizedPathname && eventMethod === requestMethod) {
matchedFunction = functionName;
console.log(`[Serverless] ✓ Matched function: ${matchedFunction}`);
break;
}
}
}
}
if (matchedFunction) {
break;
}
}
} else {
console.log('[Serverless] No functions found in config');
}
if (matchedFunction && config.functions[matchedFunction]) {
// Resolve handler path relative to output directory
const handlerPath = config.functions[matchedFunction].handler;
console.log(`[Serverless] Loading handler: ${handlerPath} from outputDir: ${outputDir}`);
const handler = await loadHandler(handlerPath, outputDir);
if (handler) {
console.log(`[Serverless] Handler loaded successfully, type: ${typeof handler}`);
const wrappedHandler = captureConsoleLogs(handler, quiet);
const event = {
body: req.body,
headers: req.headers,
httpMethod: method,
path: url,
queryStringParameters: req.query
};
const context = {
awsRequestId: 'test-request-id',
functionName: matchedFunction,
functionVersion: '$LATEST',
getRemainingTimeInMillis: ()=>30000,
invokedFunctionArn: `arn:aws:lambda:us-east-1:123456789012:function:${matchedFunction}`,
logGroupName: `/aws/lambda/${matchedFunction}`,
logStreamName: 'test-log-stream',
memoryLimitInMB: '128'
};
try {
console.log('[Serverless] Calling handler with event:', JSON.stringify(event, null, 2));
const result = await wrappedHandler(event, context);
console.log('[Serverless] Handler returned:', JSON.stringify(result, null, 2));
if (result && typeof result === 'object' && result.statusCode) {
res.status(result.statusCode);
if (result.headers) {
Object.entries(result.headers).forEach(([key, value])=>{
res.setHeader(key, String(value));
});
}
res.send(result.body);
} else {
res.json(result);
}
} catch (error) {
console.error('[Serverless] Handler error:', error.message);
console.error('[Serverless] Handler error stack:', error.stack);
log(`Handler error: ${error.message}`, 'error', false);
res.status(500).json({
error: error.message
});
}
} else {
console.error(`[Serverless] Handler not found for function: ${matchedFunction}`);
console.error(`[Serverless] Handler path: ${handlerPath}, Output dir: ${outputDir}`);
res.status(404).json({
error: 'Handler not found'
});
}
} else {
console.error(`[Serverless] Function not found for pathname: ${pathname}, method: ${method}`);
console.error(`[Serverless] Available functions: ${config.functions ? Object.keys(config.functions).join(', ') : 'none'}`);
res.status(404).json({
error: 'Function not found'
});
}
} catch (error) {
log(`Route handling error: ${error.message}`, 'error', false);
res.status(500).json({
error: error.message
});
}
});
return app;
};
const createWebSocketServer = (config, outputDir, wsPort, quiet, debug)=>{
const wss = new WebSocketServer({
port: wsPort
});
wss.on('connection', async (ws, req)=>{
log(`WebSocket connection established: ${req.url}`, 'info', false);
ws.on('message', async (message)=>{
try {
const data = JSON.parse(message.toString());
// Find matching WebSocket function
let matchedFunction = null;
if (config.functions) {
for (const [functionName, functionConfig] of Object.entries(config.functions)){
if (functionConfig.events) {
for (const event of functionConfig.events){
if (event.websocket) {
const route = event.websocket.route || '$connect';
if (route === '$default' || route === data.action) {
matchedFunction = functionName;
break;
}
}
}
}
if (matchedFunction) {
break;
}
}
}
if (matchedFunction && config.functions[matchedFunction]) {
const handler = await loadHandler(config.functions[matchedFunction].handler, outputDir);
if (handler) {
// Wrap handler with console log capture
const wrappedHandler = captureConsoleLogs(handler, quiet);
const event = {
body: data.body || null,
requestContext: {
apiGateway: {
endpoint: `ws://localhost:${wsPort}`
},
connectionId: 'test-connection-id',
routeKey: data.action || '$default'
}
};
const context = {
awsRequestId: 'test-request-id',
functionName: matchedFunction,
functionVersion: '$LATEST',
getRemainingTimeInMillis: ()=>30000,
invokedFunctionArn: `arn:aws:lambda:us-east-1:123456789012:function:${matchedFunction}`,
logGroupName: `/aws/lambda/${matchedFunction}`,
logStreamName: 'test-log-stream',
memoryLimitInMB: '128'
};
const result = await wrappedHandler(event, context);
// Handle Lambda response format for WebSocket
if (result && typeof result === 'object' && result.statusCode) {
// This is a Lambda response object, extract the body
const body = result.body || '';
ws.send(body);
} else {
// This is a direct response, stringify it
ws.send(JSON.stringify(result));
}
} else {
ws.send(JSON.stringify({
error: 'Handler not found'
}));
}
} else {
ws.send(JSON.stringify({
error: 'WebSocket function not found'
}));
}
} catch (error) {
log(`WebSocket error: ${error.message}`, 'error', false);
ws.send(JSON.stringify({
error: error.message
}));
}
});
ws.on('close', ()=>{
log('WebSocket connection closed', 'info', false);
});
});
return wss;
};
const loadEnvFile = (envPath)=>{
const envVars = {};
if (!existsSync(envPath)) {
return envVars;
}
try {
const envContent = readFileSync(envPath, 'utf8');
const lines = envContent.split('\n');
for (const line of lines){
const trimmedLine = line.trim();
// Skip empty lines and comments
if (!trimmedLine || trimmedLine.startsWith('#')) {
continue;
}
// Parse KEY=value format
const equalIndex = trimmedLine.indexOf('=');
if (equalIndex > 0) {
const key = trimmedLine.substring(0, equalIndex).trim();
const value = trimmedLine.substring(equalIndex + 1).trim();
// Remove quotes if present
const cleanValue = value.replace(/^["']|["']$/g, '');
if (key) {
envVars[key] = cleanValue;
}
}
}
} catch (error) {
log(`Warning: Could not load .env file at ${envPath}: ${error.message}`, 'warn', false);
}
return envVars;
};
export const serverless = async (cmd, callback = ()=>({}))=>{
const { cliName = 'Lex', config, debug = false, host = 'localhost', httpPort = 5000, httpsPort = 5001, quiet = false, remove = false, test = false, usePublicIp, variables, wsPort = 5002 } = cmd;
const spinner = createSpinner(quiet);
log(`${cliName} starting serverless development server...`, 'info', quiet);
await LexConfig.parseConfig(cmd);
const { outputFullPath } = LexConfig.config;
// Load environment variables from .env files
const envPaths = [
pathResolve(process.cwd(), '.env'),
pathResolve(process.cwd(), '.env.local'),
pathResolve(process.cwd(), '.env.development')
];
let envVars = {};
// Load from .env files in order (later files override earlier ones)
for (const envPath of envPaths){
const fileEnvVars = loadEnvFile(envPath);
if (Object.keys(fileEnvVars).length > 0) {
log(`Loaded environment variables from: ${envPath}`, 'info', quiet);
}
envVars = {
...envVars,
...fileEnvVars
};
}
// Start with default NODE_ENV and loaded .env variables
let variablesObj = {
NODE_ENV: 'development',
...envVars
};
// Override with command line variables if provided
if (variables) {
try {
const cliVars = JSON.parse(variables);
variablesObj = {
...variablesObj,
...cliVars
};
} catch (_error) {
log(`\n${cliName} Error: Environment variables option is not a valid JSON object.`, 'error', quiet);
callback(1);
return 1;
}
}
process.env = {
...process.env,
...variablesObj
};
// If in test mode, exit early after loading environment variables
if (test) {
log('Test mode: Environment variables loaded, exiting', 'info', quiet);
callback(0);
return 0;
}
if (remove) {
spinner.start('Cleaning output directory...');
await removeFiles(outputFullPath || '');
spinner.succeed('Successfully cleaned output directory!');
}
// Load serverless configuration
let serverlessConfig = {};
try {
// Use getPackageDir to handle npm workspaces correctly
const packageDir = getPackageDir();
// Try multiple config file formats
const configFormats = config ? [
config
] : [
pathResolve(packageDir, 'lex.config.mjs'),
pathResolve(packageDir, 'lex.config.js'),
pathResolve(packageDir, 'lex.config.cjs'),
pathResolve(packageDir, 'lex.config.ts')
];
let configPath = null;
for (const candidatePath of configFormats){
if (existsSync(candidatePath)) {
configPath = candidatePath;
break;
}
}
if (configPath) {
log(`Loading serverless config from: ${configPath}`, 'info', quiet);
const configModule = await import(configPath);
serverlessConfig = configModule.default?.serverless || configModule.serverless || {};
log('Serverless config loaded successfully', 'info', quiet);
const functionNames = Object.keys(serverlessConfig.functions || {});
log(`Loaded functions: ${functionNames.length > 0 ? functionNames.join(', ') : 'none'}`, 'info', quiet);
// Debug: Print full config if debug mode
if (debug) {
log(`Full serverless config: ${JSON.stringify(serverlessConfig, null, 2)}`, 'info', false);
}
} else {
log(`No serverless config found. Tried: ${configFormats.join(', ')}`, 'warn', quiet);
}
} catch (error) {
log(`Error loading serverless config: ${error.message}`, 'error', quiet);
if (debug) {
log(`Config error stack: ${error.stack}`, 'error', false);
}
// Don't exit, continue with empty config
}
// Merge config with command line options
const finalConfig = {
...serverlessConfig,
custom: {
'serverless-offline': {
cors: serverlessConfig.custom?.['serverless-offline']?.cors !== false,
host: serverlessConfig.custom?.['serverless-offline']?.host || host,
httpPort: serverlessConfig.custom?.['serverless-offline']?.httpPort || httpPort,
httpsPort: serverlessConfig.custom?.['serverless-offline']?.httpsPort || httpsPort,
wsPort: serverlessConfig.custom?.['serverless-offline']?.wsPort || wsPort
}
}
};
const outputDir = outputFullPath || 'lib';
log(`Using output directory: ${outputDir}`, 'info', quiet);
try {
spinner.start('Starting serverless development server...');
const httpPort = finalConfig.custom['serverless-offline'].httpPort;
const wsPort = finalConfig.custom['serverless-offline'].wsPort;
const host = finalConfig.custom['serverless-offline'].host;
log(`Creating HTTP server on ${host}:${httpPort}`, 'info', quiet);
log(`Creating WebSocket server on port ${wsPort}`, 'info', quiet);
// Create Express server
const expressApp = await createExpressServer(finalConfig, outputDir, httpPort, host, quiet, debug);
// Create WebSocket server
const wsServer = createWebSocketServer(finalConfig, outputDir, wsPort, quiet, debug);
// Handle server errors
wsServer.on('error', (error)=>{
log(`WebSocket server error: ${error.message}`, 'error', quiet);
spinner.fail('Failed to start WebSocket server.');
callback(1);
return;
});
// Start Express server
const server = expressApp.listen(httpPort, host, ()=>{
spinner.succeed('Serverless development server started.');
displayServerStatus(httpPort, finalConfig.custom['serverless-offline'].httpsPort, wsPort, host, quiet);
fetchPublicIp(usePublicIp).then((publicIp)=>{
if (publicIp) {
displayServerStatus(httpPort, finalConfig.custom['serverless-offline'].httpsPort, wsPort, host, quiet, publicIp);
}
});
});
// Handle Express server errors
server.on('error', (error)=>{
log(`Express server error: ${error.message}`, 'error', quiet);
spinner.fail('Failed to start Express server.');
callback(1);
return;
});
// Handle graceful shutdown
const shutdown = ()=>{
log('\nShutting down serverless development server...', 'info', quiet);
server.close();
wsServer.close();
callback(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// Keep the process alive
process.stdin.resume();
log('Serverless development server is running. Press Ctrl+C to stop.', 'info', quiet);
// Don't call callback here, let the process stay alive
return 0;
} catch (error) {
log(`\n${cliName} Error: ${error.message}`, 'error', quiet);
spinner.fail('Failed to start serverless development server.');
callback(1);
return 1;
}
};
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21tYW5kcy9zZXJ2ZXJsZXNzL3NlcnZlcmxlc3MudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDb3B5cmlnaHQgKGMpIDIwMTgtUHJlc2VudCwgTml0cm9nZW4gTGFicywgSW5jLlxuICogQ29weXJpZ2h0cyBsaWNlbnNlZCB1bmRlciB0aGUgTUlUIExpY2Vuc2UuIFNlZSB0aGUgYWNjb21wYW55aW5nIExJQ0VOU0UgZmlsZSBmb3IgdGVybXMuXG4gKi9cbmltcG9ydCBib3hlbiBmcm9tICdib3hlbic7XG5pbXBvcnQgY2hhbGsgZnJvbSAnY2hhbGsnO1xuaW1wb3J0IGV4cHJlc3MgZnJvbSAnZXhwcmVzcyc7XG5pbXBvcnQge3JlYWRGaWxlU3luYywgZXhpc3RzU3luYywgbWtkaXJTeW5jLCB3cml0ZUZpbGVTeW5jfSBmcm9tICdmcyc7XG5pbXBvcnQge2hvbWVkaXJ9IGZyb20gJ29zJztcbmltcG9ydCB7cmVzb2x2ZSBhcyBwYXRoUmVzb2x2ZSwgam9pbiwgaXNBYnNvbHV0ZX0gZnJvbSAncGF0aCc7XG5pbXBvcnQge3BhdGhUb0ZpbGVVUkx9IGZyb20gJ3VybCc7XG5pbXBvcnQge1dlYlNvY2tldFNlcnZlcn0gZnJvbSAnd3MnO1xuXG5pbXBvcnQge0xleENvbmZpZywgZ2V0UGFja2FnZURpcn0gZnJvbSAnLi4vLi4vTGV4Q29uZmlnLmpzJztcbmltcG9ydCB7Y3JlYXRlU3Bpbm5lciwgcmVtb3ZlRmlsZXN9IGZyb20gJy4uLy4uL3V0aWxzL2FwcC5qcyc7XG5pbXBvcnQge2xvZ30gZnJvbSAnLi4vLi4vdXRpbHMvbG9nLmpzJztcblxuZXhwb3J0IGludGVyZmFjZSBTZXJ2ZXJsZXNzT3B0aW9ucyB7XG4gIHJlYWRvbmx5IGNsaU5hbWU/OiBzdHJpbmc7XG4gIHJlYWRvbmx5IGNvbmZpZz86IHN0cmluZztcbiAgcmVhZG9ubHkgZGVidWc/OiBib29sZWFuO1xuICByZWFkb25seSBob3N0Pzogc3RyaW5nO1xuICByZWFkb25seSBodHRwUG9ydD86IG51bWJlcjtcbiAgcmVhZG9ubHkgaHR0cHNQb3J0PzogbnVtYmVyO1xuICByZWFkb25seSBxdWlldD86IGJvb2xlYW47XG4gIHJlYWRvbmx5IHJlbW92ZT86IGJvb2xlYW47XG4gIHJlYWRvbmx5IHRlc3Q/OiBib29sZWFuO1xuICByZWFkb25seSB1c2VQdWJsaWNJcD86IGJvb2xlYW47XG4gIHJlYWRvbmx5IHZhcmlhYmxlcz86IHN0cmluZztcbiAgcmVhZG9ubHkgd3NQb3J0PzogbnVtYmVyO1xufVxuXG5leHBvcnQgdHlwZSBTZXJ2ZXJsZXNzQ2FsbGJhY2sgPSAoc3RhdHVzOiBudW1iZXIpPT4gdm9pZDtcblxuaW50ZXJmYWNlIFB1YmxpY0lwQ2FjaGUge1xuICBpcDogc3RyaW5nO1xuICB0aW1lc3RhbXA6IG51bWJlcjtcbn1cblxuaW50ZXJmYWNlIFNlcnZlcmxlc3NIYW5kbGVyIHtcbiAgcmVhZG9ubHkgaGFuZGxlcjogc3RyaW5nO1xuICByZWFkb25seSBldmVudHM/OiBBcnJheTx7XG4gICAgcmVhZG9ubHkgaHR0cD86IHtcbiAgICAgIHJlYWRvbmx5IGNvcnM/OiBib29sZWFuO1xuICAgICAgcmVhZG9ubHkgbWV0aG9kPzogc3RyaW5nO1xuICAgICAgcmVhZG9ubHkgcGF0aD86IHN0cmluZztcbiAgICB9O1xuICAgIHJlYWRvbmx5IHdlYnNvY2tldD86IHtcbiAgICAgIHJlYWRvbmx5IHJvdXRlPzogc3RyaW5nO1xuICAgIH07XG4gIH0+O1xufVxuXG5pbnRlcmZhY2UgU2VydmVybGVzc0NvbmZpZyB7XG4gIHJlYWRvbmx5IGN1c3RvbT86IHtcbiAgICByZWFkb25seSAnc2VydmVybGVzcy1vZmZsaW5lJz86IHtcbiAgICAgIHJlYWRvbmx5IGNvcnM/OiBib29sZWFuO1xuICAgICAgcmVhZG9ubHkgaG9zdD86IHN0cmluZztcbiAgICAgIHJlYWRvbmx5IGh0dHBQb3J0PzogbnVtYmVyO1xuICAgICAgcmVhZG9ubHkgaHR0cHNQb3J0PzogbnVtYmVyO1xuICAgICAgcmVhZG9ubHkgd3NQb3J0PzogbnVtYmVyO1xuICAgIH07XG4gIH07XG4gIHJlYWRvbmx5IGZ1bmN0aW9ucz86IFJlY29yZDxzdHJpbmcsIFNlcnZlcmxlc3NIYW5kbGVyPjtcbn1cblxuY29uc3QgZ2V0Q2FjaGVEaXIgPSAoKTogc3RyaW5nID0+IHtcbiAgY29uc3QgY2FjaGVEaXIgPSBqb2luKGhvbWVkaXIoKSwgJy5sZXgtY2FjaGUnKTtcbiAgaWYoIWV4aXN0c1N5bmMoY2FjaGVEaXIpKSB7XG4gICAgbWtkaXJTeW5jKGNhY2hlRGlyLCB7cmVjdXJzaXZlOiB0cnVlfSk7XG4gIH1cbiAgcmV0dXJuIGNhY2hlRGlyO1xufTtcblxuY29uc3QgZ2V0Q2FjaGVQYXRoID0gKCk6IHN0cmluZyA9PiBqb2luKGdldENhY2hlRGlyKCksICdwdWJsaWMtaXAuanNvbicpO1xuXG5jb25zdCByZWFkUHVibGljSXBDYWNoZSA9ICgpOiBQdWJsaWNJcENhY2hlIHwgbnVsbCA9PiB7XG4gIGNvbnN0IGNhY2hlUGF0aCA9IGdldENhY2hlUGF0aCgpO1xuICBpZighZXhpc3RzU3luYyhjYWNoZVBhdGgpKSB7XG4gICAgcmV0dXJuIG51bGw7XG4gIH1cblxuICB0cnkge1xuICAgIGNvbnN0IGNhY2hlRGF0YSA9IHJlYWRGaWxlU3luYyhjYWNoZVBhdGgsICd1dGY4Jyk7XG4gICAgY29uc3QgY2FjaGU6IFB1YmxpY0lwQ2FjaGUgPSBKU09OLnBhcnNlKGNhY2hlRGF0YSk7XG5cbiAgICAvLyBDaGVjayBpZiBjYWNoZSBpcyBvbGRlciB0aGFuIDEgd2Vla1xuICAgIGNvbnN0IG9uZVdlZWtNcyA9IDcgKiAyNCAqIDYwICogNjAgKiAxMDAwO1xuICAgIGlmKERhdGUubm93KCkgLSBjYWNoZS50aW1lc3RhbXAgPiBvbmVXZWVrTXMpIHtcbiAgICAgIHJldHVybiBudWxsO1xuICAgIH1cblxuICAgIHJldHVybiBjYWNoZTtcbiAgfSBjYXRjaHtcbiAgICByZXR1cm4gbnVsbDtcbiAgfVxufTtcblxuY29uc3Qgd3JpdGVQdWJsaWNJcENhY2hlID0gKGlwOiBzdHJpbmcpOiB2b2lkID0+IHtcbiAgY29uc3QgY2FjaGVQYXRoID0gZ2V0Q2FjaGVQYXRoKCk7XG4gIGNvbnN0IGNhY2hlOiBQdWJsaWNJcENhY2hlID0ge1xuICAgIGlwLFxuICAgIHRpbWVzdGFtcDogRGF0ZS5ub3coKVxuICB9O1xuICB3cml0ZUZpbGVTeW5jKGNhY2hlUGF0aCwgSlNPTi5zdHJpbmdpZnkoY2FjaGUsIG51bGwsIDIpKTtcbn07XG5cbmNvbnN0IGZldGNoUHVibGljSXAgPSAoZm9yY2VSZWZyZXNoOiBib29sZWFuID0gZmFsc2UpOiBQcm9taXNlPHN0cmluZyB8IHVuZGVmaW5lZD4gPT4gbmV3IFByb21pc2UoKHJlc29sdmUpID0+IHtcbiAgaWYoIWZvcmNlUmVmcmVzaCkge1xuICAgIGNvbnN0IGNhY2hlZCA9IHJlYWRQdWJsaWNJcENhY2hlKCk7XG4gICAgaWYoY2FjaGVkKSB7XG4gICAgICByZXNvbHZlKGNhY2hlZC5pcCk7XG4gICAgICByZXR1cm47XG4gICAgfVxuICB9XG5cbiAgLy8gVXNlIGZldGNoIGluc3RlYWQgb2YgaHR0cHNcbiAgZmV0Y2goJ2h0dHBzOi8vYXBpLmlwaWZ5Lm9yZycpXG4gICAgLnRoZW4oKHJlcykgPT4gcmVzLnRleHQoKSlcbiAgICAudGhlbigoZGF0YSkgPT4ge1xuICAgICAgY29uc3QgaXAgPSBkYXRhLnRyaW0oKTtcbiAgICAgIGlmKGlwKSB7XG4gICAgICAgIHdyaXRlUHVibGljSXBDYWNoZShpcCk7XG4gICAgICB9XG4gICAgICByZXNvbHZlKGlwKTtcbiAgICB9KVxuICAgIC5jYXRjaCgoKSA9PiByZXNvbHZlKHVuZGVmaW5lZCkpO1xufSk7XG5cbmNvbnN0IGRpc3BsYXlTZXJ2ZXJTdGF0dXMgPSAoXG4gIGh0dHBQb3J0OiBudW1iZXIsXG4gIGh0dHBzUG9ydDogbnVtYmVyLFxuICB3c1BvcnQ6IG51bWJlcixcbiAgaG9zdDogc3RyaW5nLFxuICBxdWlldDogYm9vbGVhbixcbiAgcHVibGljSXA/OiBzdHJpbmdcbikgPT4ge1xuICBpZihxdWlldCkge1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGNvbnN0IGh0dHBVcmwgPSBgaHR0cDovLyR7aG9zdH06JHtodHRwUG9ydH1gO1xuICBjb25zdCBodHRwc1VybCA9IGBodHRwczovLyR7aG9zdH06JHtodHRwc1BvcnR9YDtcbiAgY29uc3Qgd3NVcmwgPSBgd3M6Ly8ke2hvc3R9OiR7d3NQb3J0fWA7XG4gIGNvbnN0IHdzc1VybCA9IGB3c3M6Ly8ke2hvc3R9OiR7d3NQb3J0fWA7XG5cbiAgbGV0IHVybExpbmVzID0gYCR7Y2hhbGsuZ3JlZW4oJ0hUVFA6Jyl9ICAgICAgJHtjaGFsay51bmRlcmxpbmUoaHR0cFVybCl9XFxuYDtcbiAgdXJsTGluZXMgKz0gYCR7Y2hhbGsuZ3JlZW4oJ0hUVFBTOicpfSAgICAgJHtjaGFsay51bmRlcmxpbmUoaHR0cHNVcmwpfVxcbmA7XG4gIHVybExpbmVzICs9IGAke2NoYWxrLmdyZWVuKCdXZWJTb2NrZXQ6Jyl9ICR7Y2hhbGsudW5kZXJsaW5lKHdzVXJsKX1cXG5gO1xuICB1cmxMaW5lcyArPSBgJHtjaGFsay5ncmVlbignV1NTOicpfSAgICAgICAke2NoYWxrLnVuZGVybGluZSh3c3NVcmwpfVxcbmA7XG5cbiAgaWYocHVibGljSXApIHtcbiAgICB1cmxMaW5lcyArPSBgXFxuJHtjaGFsay5ncmVlbignUHVibGljOicpfSAgICAke2NoYWxrLnVuZGVybGluZShgaHR0cDovLyR7cHVibGljSXB9OiR7aHR0cFBvcnR9YCl9XFxuYDtcbiAgfVxuXG4gIGNvbnN0IHN0YXR1c0JveCA9IGJveGVuKFxuICAgIGAke2NoYWxrLmN5YW4uYm9sZCgn8J+agCBTZXJ2ZXJsZXNzIERldmVsb3BtZW50IFNlcnZlciBSdW5uaW5nJyl9XFxuXFxuJHt1cmxMaW5lc31cXG5gICtcbiAgICBgJHtjaGFsay55ZWxsb3coJ1ByZXNzIEN0cmwrQyB0byBzdG9wIHRoZSBzZXJ2ZXInKX1gLFxuICAgIHtcbiAgICAgIGJhY2tncm91bmRDb2xvcjogJyMxYTFhMWEnLFxuICAgICAgYm9yZGVyQ29sb3I6ICdjeWFuJyxcbiAgICAgIGJvcmRlclN0eWxlOiAncm91bmQnLFxuICAgICAgbWFyZ2luOiAxLFxuICAgICAgcGFkZGluZzogMVxuICAgIH1cbiAgKTtcblxuICBjb25zb2xlLmxvZyhgXFxuJHtzdGF0dXNCb3h9XFxuYCk7XG59O1xuXG5jb25zdCBsb2FkSGFuZGxlciA9IGFzeW5jIChoYW5kbGVyUGF0aDogc3RyaW5nLCBvdXRwdXREaXI6IHN0cmluZykgPT4ge1xuICB0cnkge1xuICAgIGNvbnNvbGUubG9nKGBbU2VydmVybGVzc10gUGFyc2luZyBoYW5kbGVyIHBhdGg6ICR7aGFuZGxlclBhdGh9YCk7XG5cbiAgICAvLyBQYXJzZSBBV1MgTGFtYmRhIGhhbmRsZXIgZm9ybWF0OiBcInBhdGgvdG8vZmlsZS5leHBvcnROYW1lXCIgb3IgXCJmaWxlLmV4cG9ydE5hbWVcIlxuICAgIC8vIEV4YW1wbGVzOiBcImluZGV4LmhhbmRsZXJcIiwgXCJoYW5kbGVycy9hcGkuaGFuZGxlclwiLCBcInNyYy9pbmRleC5kZWZhdWx0XCJcbiAgICBjb25zdCBoYW5kbGVyUGFydHMgPSBoYW5kbGVyUGF0aC5zcGxpdCgnLicpO1xuICAgIGNvbnNvbGUubG9nKCdbU2VydmVybGVzc10gSGFuZGxlciBwYXJ0cyBhZnRlciBzcGxpdDonLCBoYW5kbGVyUGFydHMpO1xuXG4gICAgbGV0IGZpbGVQYXRoOiBzdHJpbmc7XG4gICAgbGV0IGV4cG9ydE5hbWU6IHN0cmluZyB8IG51bGwgPSBudWxsO1xuXG4gICAgaWYoaGFuZGxlclBhcnRzLmxlbmd0aCA+IDEpIHtcbiAgICAgIC8vIEFXUyBMYW1iZGEgZm9ybWF0OiBcImZpbGUuZXhwb3J0TmFtZVwiXG4gICAgICAvLyBUYWtlIHRoZSBsYXN0IHBhcnQgYXMgZXhwb3J0IG5hbWUsIHJlc3QgYXMgZmlsZSBwYXRoXG4gICAgICAvLyBIYW5kbGUgY2FzZXMgbGlrZSBcImluZGV4LmhhbmRsZXJcIiAtPiBmaWxlOiBcImluZGV4XCIsIGV4cG9ydDogXCJoYW5kbGVyXCJcbiAgICAgIC8vIE9yIFwiaGFuZGxlcnMvYXBpLmhhbmRsZXJcIiAtPiBmaWxlOiBcImhhbmRsZXJzL2FwaVwiLCBleHBvcnQ6IFwiaGFuZGxlclwiXG4gICAgICBleHBvcnROYW1lID0gaGFuZGxlclBhcnRzW2hhbmRsZXJQYXJ0cy5sZW5ndGggLSAxXSB8fCBudWxsO1xuICAgICAgZmlsZVBhdGggPSBoYW5kbGVyUGFydHMuc2xpY2UoMCwgLTEpLmpvaW4oJy4nKTtcbiAgICAgIGNvbnNvbGUubG9nKGBbU2VydmVybGVzc10gUGFyc2VkIEFXUyBMYW1iZGEgZm9ybWF0IC0gZmlsZVBhdGg6IFwiJHtmaWxlUGF0aH1cIiwgZXhwb3J0TmFtZTogXCIke2V4cG9ydE5hbWV9XCJgKTtcbiAgICB9IGVsc2Uge1xuICAgICAgLy8gU2ltcGxlIGZvcm1hdDoganVzdCB0aGUgZmlsZSBwYXRoXG4gICAgICBmaWxlUGF0aCA9IGhhbmRsZXJQYXRoO1xuICAgICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBTaW1wbGUgZm9ybWF0IC0gZmlsZVBhdGg6IFwiJHtmaWxlUGF0aH1cImApO1xuICAgIH1cblxuICAgIC8vIEVuc3VyZSBmaWxlUGF0aCBkb2Vzbid0IGhhdmUgdGhlIGV4cG9ydCBuYW1lIGluIGl0XG4gICAgaWYoZmlsZVBhdGguaW5jbHVkZXMoJy5oYW5kbGVyJykgfHwgZmlsZVBhdGguaW5jbHVkZXMoJy5kZWZhdWx0JykpIHtcbiAgICAgIGNvbnNvbGUuZXJyb3IoYFtTZXJ2ZXJsZXNzXSBXQVJOSU5HOiBmaWxlUGF0aCBzdGlsbCBjb250YWlucyBleHBvcnQgbmFtZSEgZmlsZVBhdGg6IFwiJHtmaWxlUGF0aH1cImApO1xuICAgICAgLy8gVHJ5IHRvIGZpeCBpdCAtIHJlbW92ZSB0aGUgbGFzdCBwYXJ0IGlmIGl0IGxvb2tzIGxpa2UgYW4gZXhwb3J0IG5hbWVcbiAgICAgIGNvbnN0IHBhdGhQYXJ0cyA9IGZpbGVQYXRoLnNwbGl0KCcuJyk7XG4gICAgICBpZihwYXRoUGFydHMubGVuZ3RoID4gMSkge1xuICAgICAgICBjb25zdCBsYXN0UGFydCA9IHBhdGhQYXJ0c1twYXRoUGFydHMubGVuZ3RoIC0gMV07XG4gICAgICAgIGlmKFsnaGFuZGxlcicsICdkZWZhdWx0JywgJ2luZGV4J10uaW5jbHVkZXMobGFzdFBhcnQpICYmICFleHBvcnROYW1lKSB7XG4gICAgICAgICAgZXhwb3J0TmFtZSA9IGxhc3RQYXJ0O1xuICAgICAgICAgIGZpbGVQYXRoID0gcGF0aFBhcnRzLnNsaWNlKDAsIC0xKS5qb2luKCcuJyk7XG4gICAgICAgICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBGaXhlZCAtIGZpbGVQYXRoOiBcIiR7ZmlsZVBhdGh9XCIsIGV4cG9ydE5hbWU6IFwiJHtleHBvcnROYW1lfVwiYCk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICAvLyBIYW5kbGUgYm90aCByZWxhdGl2ZSBwYXRocyBhbmQgYWJzb2x1dGUgcGF0aHNcbiAgICBsZXQgZnVsbFBhdGg6IHN0cmluZztcbiAgICBpZihpc0Fic29sdXRlKGZpbGVQYXRoKSkge1xuICAgICAgZnVsbFBhdGggPSBmaWxlUGF0aDtcbiAgICB9IGVsc2Uge1xuICAgICAgZnVsbFBhdGggPSBwYXRoUmVzb2x2ZShvdXRwdXREaXIsIGZpbGVQYXRoKTtcbiAgICB9XG4gICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBSZXNvbHZlZCBmdWxsUGF0aCAoYmVmb3JlIGV4dGVuc2lvbnMpOiAke2Z1bGxQYXRofWApO1xuXG4gICAgLy8gVHJ5IGRpZmZlcmVudCBleHRlbnNpb25zIGlmIGZpbGUgZG9lc24ndCBleGlzdFxuICAgIGlmKCFleGlzdHNTeW5jKGZ1bGxQYXRoKSkge1xuICAgICAgY29uc3QgZXh0ZW5zaW9ucyA9IFsnLmpzJywgJy5tanMnLCAnLmNqcyddO1xuICAgICAgY29uc3QgcGF0aFdpdGhvdXRFeHQgPSBmdWxsUGF0aC5yZXBsYWNlKC9cXC4oanN8bWpzfGNqcykkLywgJycpO1xuICAgICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBUcnlpbmcgZXh0ZW5zaW9ucy4gQmFzZSBwYXRoOiAke3BhdGhXaXRob3V0RXh0fWApO1xuICAgICAgZm9yKGNvbnN0IGV4dCBvZiBleHRlbnNpb25zKSB7XG4gICAgICAgIGNvbnN0IGNhbmRpZGF0ZVBhdGggPSBwYXRoV2l0aG91dEV4dCArIGV4dDtcbiAgICAgICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBDaGVja2luZzogJHtjYW5kaWRhdGVQYXRofSAoZXhpc3RzOiAke2V4aXN0c1N5bmMoY2FuZGlkYXRlUGF0aCl9KWApO1xuICAgICAgICBpZihleGlzdHNTeW5jKGNhbmRpZGF0ZVBhdGgpKSB7XG4gICAgICAgICAgZnVsbFBhdGggPSBjYW5kaWRhdGVQYXRoO1xuICAgICAgICAgIGNvbnNvbGUubG9nKGBbU2VydmVybGVzc10gRm91bmQgZmlsZSB3aXRoIGV4dGVuc2lvbjogJHtmdWxsUGF0aH1gKTtcbiAgICAgICAgICBicmVhaztcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICBjb25zb2xlLmxvZyhgW1NlcnZlcmxlc3NdIEZpbGUgZXhpc3RzIHdpdGhvdXQgdHJ5aW5nIGV4dGVuc2lvbnM6ICR7ZnVsbFBhdGh9YCk7XG4gICAgfVxuXG4gICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBGaW5hbCBmdWxsUGF0aDogJHtmdWxsUGF0aH1gKTtcbiAgICBjb25zb2xlLmxvZyhgW1NlcnZlcmxlc3NdIEV4cG9ydCBuYW1lOiAke2V4cG9ydE5hbWUgfHwgJ2RlZmF1bHQvaGFuZGxlcid9YCk7XG4gICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBGaWxlIGV4aXN0czogJHtleGlzdHNTeW5jKGZ1bGxQYXRoKX1gKTtcblxuICAgIGlmKCFleGlzdHNTeW5jKGZ1bGxQYXRoKSkge1xuICAgICAgY29uc29sZS5lcnJvcihgW1NlcnZlcmxlc3NdIEhhbmRsZXIgZmlsZSBub3QgZm91bmQ6ICR7ZnVsbFBhdGh9YCk7XG4gICAgICBjb25zb2xlLmVycm9yKGBbU2VydmVybGVzc10gT3V0cHV0IGRpcmVjdG9yeTogJHtvdXRwdXREaXJ9YCk7XG4gICAgICBjb25zb2xlLmVycm9yKGBbU2VydmVybGVzc10gSGFuZGxlciBwYXRoIGZyb20gY29uZmlnOiAke2hhbmRsZXJQYXRofWApO1xuICAgICAgdGhyb3cgbmV3IEVycm9yKGBIYW5kbGVyIGZpbGUgbm90IGZvdW5kOiAke2Z1bGxQYXRofWApO1xuICAgIH1cblxuICAgIC8vIER5bmFtaWMgaW1wb3J0IG9mIHRoZSBoYW5kbGVyIHdpdGggYmV0dGVyIGVycm9yIGhhbmRsaW5nXG4gICAgLy8gQWRkIC5qcyBleHRlbnNpb24gaWYgaW1wb3J0aW5nIFR5cGVTY3JpcHQgY29tcGlsZWQgb3V0cHV0XG4gICAgY29uc3QgaW1wb3J0UGF0aCA9IGZ1bGxQYXRoLmVuZHNXaXRoKCcudHMnKSA/IGZ1bGxQYXRoLnJlcGxhY2UoL1xcLnRzJC8sICcuanMnKSA6IGZ1bGxQYXRoO1xuXG4gICAgdHJ5IHtcbiAgICAgIC8vIENvbnZlcnQgdG8gZmlsZTovLyBVUkwgZm9yIEVTIG1vZHVsZSBpbXBvcnRzIChyZXF1aXJlZCBmb3IgYWJzb2x1dGUgcGF0aHMpXG4gICAgICAvLyBVc2UgcGF0aFRvRmlsZVVSTCB0byBlbnN1cmUgcHJvcGVyIGZpbGU6Ly8gVVJMIGZvcm1hdFxuICAgICAgY29uc3QgaW1wb3J0VXJsID0gcGF0aFRvRmlsZVVSTChpbXBvcnRQYXRoKS5ocmVmO1xuICAgICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBJbXBvcnRpbmcgaGFuZGxlciBmcm9tOiAke2ltcG9ydFVybH1gKTtcbiAgICAgIGNvbnNvbGUubG9nKGBbU2VydmVybGVzc10gRmlsZSBwYXRoOiAke2ltcG9ydFBhdGh9YCk7XG5cbiAgICAgIC8vIFVzZSBpbXBvcnQoKSB3aXRoIHRoZSBmaWxlIFVSTFxuICAgICAgLy8gTm90ZTogSWYgdGhlIGhhbmRsZXIgZmlsZSBoYXMgaW1wb3J0IGVycm9ycyAobGlrZSBtaXNzaW5nIGRlcGVuZGVuY2llcyksXG4gICAgICAvLyB0aG9zZSB3aWxsIHN1cmZhY2UgaGVyZSwgYnV0IHRoYXQncyBhIGhhbmRsZXIgY29kZSBpc3N1ZSwgbm90IGEgbG9hZGVyIGlzc3VlXG4gICAgICBjb25zdCBoYW5kbGVyTW9kdWxlID0gYXdhaXQgaW1wb3J0KGltcG9ydFVybCk7XG