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