UNPKG

@dscodotco/theme-cli

Version:

A CLI tool for developing Shopify themes

178 lines (177 loc) 6.93 kB
// @ts-nocheck import express from "express"; import path from "path"; import { ShopifyRenderer } from "./renderer.js"; import { createLogger } from "../logger.js"; import { ThemeAPI } from "./theme-api.js"; import { ThemeChecker } from "./theme-checker.js"; import { devUIHtml, devUIStyles, errorPageTemplate, devUIScript, } from "./templates/index.js"; const logger = createLogger("dev-server"); /** * A simple development server for Shopify themes * Provides a UI for browsing theme files and previewing them */ export class SimpleDevServer { constructor(options) { this.server = null; this.app = express(); this.debug = options.debug || false; this.renderer = new ShopifyRenderer({ credentials: options.credentials, themeId: options.themeId, shopify: options.shopify, debug: this.debug, }); this.themeDir = options.themeDir; this.port = options.port || 3000; this.themeManager = options.themeManager; this.credentials = options.credentials; this.themeId = options.themeId; this.themeAPI = new ThemeAPI(options.shopify, this.themeManager, this.themeDir); this.themeChecker = new ThemeChecker(this.themeDir); // Load templates this.devUIHtml = devUIHtml; this.devUIStyles = devUIStyles; this.errorPageTemplate = errorPageTemplate; this.configureMiddleware(); this.configureRoutes(); this.configureErrorHandling(); if (this.debug) { logger.info("Debug mode enabled"); logger.info(`Theme directory: ${this.themeDir}`); logger.info(`Theme ID: ${this.themeId}`); logger.info(`Store: ${this.credentials.storeName}`); } } configureMiddleware() { this.app.use(express.json()); this.app.use(express.static("public")); if (this.debug) { // Add request logging middleware this.app.use((req, res, next) => { logger.info(`${req.method} ${req.path}`); next(); }); } } configureRoutes() { this.app.get("/dev-ui.css", (req, res) => { res.type("text/css").send(devUIStyles); }); this.app.get("/dev-ui.js", (req, res) => { res.type("application/javascript").send(devUIScript); }); this.app.get("/api/theme/metadata", async (req, res) => { try { const metadata = await this.renderer.getThemeMetadata(); res.json(metadata); } catch (err) { const error = err; this.logError("Failed to get theme metadata", error); res.status(500).json({ error: error.message }); } }); this.app.get("/api/theme/pages", async (req, res) => { try { const pages = await this.renderer.getThemePages(); res.json(pages); } catch (err) { const error = err; this.logError("Failed to get theme pages", error); res.status(500).json({ error: error.message }); } }); this.app.get("/api/theme/page/:path(*)", async (req, res) => { try { if (this.debug) { logger.info(`Getting page metadata for path: ${req.params.path}`); } const page = await this.renderer.getPageMetadata(req.params.path); if (!page) { if (this.debug) { logger.warn(`No page metadata found for path: ${req.params.path}`); } res.status(404).json({ error: "Page not found" }); return; } const templatePath = path.join(this.renderer.themePath, "templates", page.template + ".json"); if (this.debug) { logger.info(`Looking for template at: ${templatePath}`); } if (!this.renderer.fileExists(templatePath)) { const error = new Error(`Template file not found: ${templatePath}`); throw error; } const templateContent = await this.renderer.renderTemplate(templatePath); res.json({ ...page, content: templateContent }); } catch (err) { const error = err; this.logError(`Failed to get page metadata for ${req.params.path}`, error); res.status(500).json({ error: error.message }); } }); this.app.get("*", async (req, res) => { try { const page = await this.renderer.getPageMetadata(req.path); const attributes = page?.attributes ? " " + page.attributes : ""; const currentPage = encodeURIComponent(req.path); const html = devUIHtml.replace("<body>", `<body${attributes} data-current-page="${currentPage}">`); res.send(html); } catch (error) { this.handleError(error, req, res); } }); } configureErrorHandling() { this.app.use((err, req, res) => { this.handleError(err, req, res); }); } logError(message, error) { logger.error(`${message}: ${error.message}`); if (this.debug && error.stack) { logger.error("Stack trace:"); logger.error(error.stack); } } handleError(error, req, res) { this.logError("Server error", error); const errorPage = errorPageTemplate .replace("{{message}}", error.message) .replace("{{stack}}", this.debug ? error.stack || "" : ""); res.status(500).send(errorPage); } /** * Starts the development server * @returns Promise that resolves with the server URL */ start() { return new Promise((resolve) => { this.server = this.app.listen(this.port, () => { const serverUrl = `http://localhost:${this.port}`; logger.success(`Development server running at ${serverUrl}`); if (this.debug) { logger.info("Debug mode enabled - verbose logging is active"); } console.log(`API endpoints:`); console.log(` - GET /api/theme/metadata`); console.log(` - GET /api/theme/pages`); console.log(` - GET /api/theme/page/:path`); resolve(serverUrl); }); }); } /** * Stops the development server */ stop() { if (this.server) { this.server.close(); logger.info("Development server stopped"); } } }