UNPKG

@dscodotco/theme-cli

Version:

A CLI tool for developing Shopify themes

259 lines (228 loc) 7.37 kB
// @ts-nocheck import express from "express"; import fs from "fs"; import path from "path"; import { ShopifyRenderer, RendererOptions } from "./renderer.js"; import { ShopifyCredentials } from "./theme-manager.js"; import { createLogger } from "../logger.js"; import Shopify from "shopify-api-node"; import { ThemeManager } from "./theme-manager.js"; import { ThemeAPI } from "./theme-api.js"; import { ThemeChecker, ThemeCheckResult } from "./theme-checker.js"; import { devUIHtml, devUIStyles, errorPageTemplate, devUIScript, } from "./templates/index.js"; const logger = createLogger("dev-server"); interface DevServerOptions { port: number; themeDir: string; shopify: Shopify; themeManager: ThemeManager; credentials: { storeName: string; apiKey: string; password: string; }; themeId: number; debug?: boolean; } /** * A simple development server for Shopify themes * Provides a UI for browsing theme files and previewing them */ export class SimpleDevServer { private app: express.Application; private renderer: ShopifyRenderer; private themeDir: string; private port: number; private server: ReturnType<typeof this.app.listen> | null = null; private themeAPI: ThemeAPI; private themeManager: ThemeManager; private credentials: DevServerOptions["credentials"]; private themeId: number; private themeChecker: ThemeChecker; private devUIHtml: string; private devUIStyles: string; private errorPageTemplate: string; private debug: boolean; constructor(options: DevServerOptions) { 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}`); } } private configureMiddleware(): void { 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(); }); } } private configureRoutes(): void { 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 as Error; 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 as Error; 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 as Error; 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 as Error, req, res); } }); } private configureErrorHandling(): void { this.app.use((err: Error, req: express.Request, res: express.Response) => { this.handleError(err, req, res); }); } private logError(message: string, error: Error): void { logger.error(`${message}: ${error.message}`); if (this.debug && error.stack) { logger.error("Stack trace:"); logger.error(error.stack); } } private handleError( error: Error, req: express.Request, res: express.Response ): void { 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(): Promise<string> { 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(): void { if (this.server) { this.server.close(); logger.info("Development server stopped"); } } }