@dscodotco/theme-cli
Version:
A CLI tool for developing Shopify themes
178 lines (177 loc) • 6.93 kB
JavaScript
// @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");
}
}
}