@dscodotco/theme-cli
Version:
A CLI tool for developing Shopify themes
259 lines (228 loc) • 7.37 kB
text/typescript
// @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");
}
}
}