UNPKG

ryuu

Version:

Domo App Dev Studio CLI, The main tool used to create, edit, and publish app designs to Domo

239 lines 11.7 kB
"use strict"; /** * Vite plugin to integrate Domo app development features * - Proxy for Domo data API * - Static file serving for app assets * - Auth validation * - File watching for hot reload */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.domoPlugin = domoPlugin; const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const ryuu_proxy_1 = require("@domoinc/ryuu-proxy"); function domoPlugin(options) { let proxy; const sseClients = []; let cachedManifestData = null; let dataFetchPromise; // Start fetching manifest data immediately const { client, manifest } = options; dataFetchPromise = (async () => { try { const appData = await client.getDomoappsData(manifest, manifest?.proxyId); // Calculate sizes consistent with Domo Web const sizeMultiplier = { width: 235, height: 290, }; const width = sizeMultiplier.width * manifest.size.width - 10; const height = sizeMultiplier.height * manifest.size.height - 40; const userId = options.userId || appData.user.id; const customerId = appData.customer || 1; // Cache data for use in transformIndexHtml cachedManifestData = { manifest, width, height, userId, customerId, instance: client.getInstance(), }; } catch (err) { console.error('Error fetching Domo app data:', err); if (err.statusCode === 401 || err.statusCode === 403) { console.error('\n❌ Authentication failed. Your session may have expired.'); console.error(' Please run "domo login" to authenticate again.\n'); } cachedManifestData = {}; } })().catch((err) => { // Additional top-level catch to prevent unhandled promise rejection console.error('Unexpected error in data fetch promise:', err); }); return { name: 'vite-plugin-domo', configureServer(viteServer) { // Serve favicon files from public directory // This must run before other middleware to prevent 404s viteServer.middlewares.use((req, res, next) => { if (req.url === '/favicon.ico' || req.url === '/favicon.svg') { const faviconPath = path_1.default.join(viteServer.config.publicDir, req.url === '/favicon.ico' ? 'favicon.ico' : 'favicon.svg'); if (fs_1.default.existsSync(faviconPath)) { const content = fs_1.default.readFileSync(faviconPath); res.setHeader('Content-Type', 'image/svg+xml'); res.setHeader('Cache-Control', 'public, max-age=31536000'); res.end(content); return; } } next(); }); // Initialize proxy if we don't have a proxyId if (!manifest.proxyId) { if (!manifest.id) { console.warn('\n⚠️ Warning: No design ID found in manifest.json.'); console.warn(' Advanced data proxy features (AppDB, Files, Code Engine) will not be available.'); console.warn(' To enable them, publish your app with "domo publish" and a proxyId will be added automatically.\n'); } else { client .createApp(manifest.id) .then((app) => { manifest.proxyId = app.instance.id; proxy = new ryuu_proxy_1.Proxy({ manifest }, manifest.proxyId); console.log(`✓ Created temporary app instance with proxyId: ${manifest.proxyId}`); }) .catch((err) => { console.error('\n❌ Failed to create app instance for proxy:'); if (err.statusCode === 404) { console.error(` Design ID "${manifest.id}" not found. Please publish your app first with 'domo publish'.`); } else if (err.statusCode === 403) { console.error(` Permission denied. You may not be an owner of design "${manifest.id}".`); } else { console.error(` ${err.message || err}`); } console.error('\n Note: Advanced data proxy features (AppDB, Files, Code Engine) will not work.'); console.error(' To enable them, publish your app and add the proxyId to your manifest.\n'); }); } } else { proxy = new ryuu_proxy_1.Proxy({ manifest }, manifest.proxyId); } // Watch app files for changes const appDir = process.cwd(); const watcher = fs_1.default.watch(appDir, { recursive: true }, (_eventType, filename) => { if (filename) { // Filter out non-app files (node_modules, .git, etc.) if (!filename.includes('node_modules') && !filename.includes('.git') && !filename.startsWith('.') && (filename.endsWith('.html') || filename.endsWith('.js') || filename.endsWith('.css') || filename.endsWith('.jsx') || filename.endsWith('.ts') || filename.endsWith('.tsx'))) { console.log(`[File changed] ${filename}`); // Notify all SSE clients sseClients.forEach(client => { client.write('data: reload\n\n'); }); } } }); // Clean up watcher when server closes viteServer.httpServer?.on('close', () => { watcher.close(); }); // Register as PRE middlewares (run BEFORE Vite's built-in middlewares) // This is critical so we can serve user app files without Vite transformation // SSE endpoint for file watching viteServer.middlewares.use('/__file_watcher', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); // Add this client to the list sseClients.push(res); // Remove client when connection closes req.on('close', () => { const index = sseClients.indexOf(res); if (index !== -1) { sseClients.splice(index, 1); } }); }); // Add Domo proxy middleware for data API calls // MUST be registered here (not in return function) to run before Vite's HTML fallback viteServer.middlewares.use((req, res, next) => { if (proxy && proxy.express) { // Add Express-compatible methods to Node.js response object // ryuu-proxy expects Express Response, but Vite provides Node.js ServerResponse if (!('status' in res)) { res.status = function (code) { this.statusCode = code; return this; }; } if (!('send' in res)) { res.send = function (data) { if (typeof data === 'string') { this.setHeader('Content-Type', 'text/html'); this.end(data); } else if (typeof data === 'object') { this.setHeader('Content-Type', 'application/json'); this.end(JSON.stringify(data)); } else { this.end(data); } return this; }; } const proxyMiddleware = proxy.express(); proxyMiddleware(req, res, next); } else { next(); } }); // Serve app files from the current working directory with /app prefix // MUST be registered here (not in return function) to run before Vite's HTML transform viteServer.middlewares.use('/app', (req, res, next) => { // When using path-specific middleware, Express already strips the prefix // So req.url is already without '/app' const urlPath = req.url || ''; // Remove query string for file path lookup const filePathPart = urlPath.split('?')[0]; // path.join handles cross-platform path separators automatically const filePath = path_1.default.join(process.cwd(), filePathPart); // Check if file exists if (fs_1.default.existsSync(filePath) && fs_1.default.statSync(filePath).isFile()) { // Serve file directly with correct MIME type const ext = path_1.default.extname(filePath); const mimeTypes = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', }; const contentType = mimeTypes[ext] || 'application/octet-stream'; res.setHeader('Content-Type', contentType); // Read and send file const fileContent = fs_1.default.readFileSync(filePath); res.end(fileContent); } else { next(); } }); }, async transformIndexHtml(html, ctx) { // Only transform the wrapper UI's index.html, not the user's app HTML // The user's app is served under /app/ path if (ctx.path.startsWith('/app/')) { return html; // Don't transform user app HTML } // Wait for manifest data to be fetched before transforming HTML await dataFetchPromise; // Inject manifest data into HTML for React to pick up const manifestData = cachedManifestData || {}; return html.replace('</head>', `<script>window.__DOMO_DEV_DATA__ = ${JSON.stringify(manifestData)};</script></head>`); }, }; } //# sourceMappingURL=vite-plugin-domo.js.map