@wp-forge/cli
Version:
CLI tool for WP-Forge - create and manage WordPress themes
984 lines (916 loc) • 33.8 kB
JavaScript
#!/usr/bin/env node
// src/index.ts
import { Command } from "commander";
import chalk10 from "chalk";
// src/commands/dev.ts
import chalk from "chalk";
import ora from "ora";
import { execa } from "execa";
async function devCommand(options) {
console.log(chalk.bold.cyan("\u2692\uFE0F WP-Forge Development Server\n"));
const spinner = ora("Starting Vite dev server...").start();
try {
const viteProcess = execa("vite", ["--port", options.port, "--host", options.host], {
stdio: "inherit"
});
spinner.succeed("Development server started!");
console.log();
console.log(chalk.green(" \u279C") + " Local: " + chalk.cyan(`http://${options.host}:${options.port}`));
console.log(chalk.green(" \u279C") + " Hot reload enabled");
console.log(chalk.green(" \u279C") + " TypeScript checking in background");
console.log();
console.log(chalk.dim(" Press Ctrl+C to stop"));
console.log();
await viteProcess;
} catch (error) {
spinner.fail("Failed to start development server");
console.error(error);
process.exit(1);
}
}
// src/commands/build.ts
import chalk2 from "chalk";
import ora2 from "ora";
import { execa as execa2 } from "execa";
async function buildCommand(options) {
console.log(chalk2.bold.cyan("\u2692\uFE0F Building for Production\n"));
const spinner = ora2("Building theme...").start();
try {
const args = ["build"];
if (options.analyze) {
args.push("--mode", "analyze");
}
await execa2("vite", args, { stdio: "inherit" });
spinner.succeed(chalk2.green("Build complete!"));
console.log();
console.log(chalk2.cyan("\u{1F4E6} Output:") + " dist/");
console.log(chalk2.dim(" Ready for production deployment"));
console.log();
} catch (error) {
spinner.fail("Build failed");
console.error(error);
process.exit(1);
}
}
// src/commands/block.ts
import chalk3 from "chalk";
import ora3 from "ora";
import fs from "fs-extra";
import path from "path";
async function blockCommand(name, options) {
console.log(chalk3.bold.cyan("\u2692\uFE0F Creating Block\n"));
console.log(chalk3.dim(`Name: ${name}`));
console.log(chalk3.dim(`Type: ${options.type}`));
console.log(chalk3.dim(`Category: ${options.category}
`));
const spinner = ora3("Generating block...").start();
try {
const slug = name.toLowerCase().replace(/\s+/g, "-");
const blockDir = path.join(process.cwd(), "src/blocks", slug);
await fs.ensureDir(blockDir);
await generateBlockConfig(blockDir, name, slug, options);
await generateBlockEdit(blockDir, name, slug, options.styleFramework || "none");
await generateBlockRender(blockDir, name, slug, options.type, options.styleFramework || "none");
await generateBlockStyles(blockDir, slug, options.styleFramework || "none");
spinner.succeed(chalk3.green(`Block "${name}" created!`));
console.log();
console.log(chalk3.cyan("\u{1F4C1} Files created:"));
console.log(chalk3.dim(` src/blocks/${slug}/block.json`));
console.log(chalk3.dim(` src/blocks/${slug}/edit.tsx`));
console.log(chalk3.dim(` src/blocks/${slug}/render.${options.type === "dynamic" ? "php" : "tsx"}`));
console.log(chalk3.dim(` src/blocks/${slug}/style.css`));
console.log();
} catch (error) {
spinner.fail("Failed to create block");
console.error(error);
process.exit(1);
}
}
async function generateBlockConfig(blockDir, name, slug, options) {
const config = {
$schema: "https://schemas.wp.org/trunk/block.json",
apiVersion: 3,
name: `wp-forge/${slug}`,
title: name,
category: options.category,
icon: "smiley",
description: `${name} block`,
supports: {
html: false,
align: true
},
attributes: {},
editorScript: "file:./edit.tsx",
...options.type === "dynamic" && { render: "file:./render.php" },
style: "file:./style.css"
};
await fs.writeJson(path.join(blockDir, "block.json"), config, { spaces: 2 });
}
async function generateBlockEdit(blockDir, name, slug, framework) {
const useTailwind = framework === "tailwind";
const useUno = framework === "unocss";
const classes = useTailwind || useUno ? 'className="p-4 bg-gray-100 rounded-lg"' : "";
const headingClasses = useTailwind || useUno ? 'className="text-2xl font-bold mb-2"' : "";
const content = `import { useBlockProps } from '@wordpress/block-editor'
export default function Edit() {
const blockProps = useBlockProps(${useTailwind || useUno ? "{ className: 'p-4 bg-gray-100 rounded-lg' }" : ""})
return (
<div {...blockProps}>
<h3 ${headingClasses}>${name}</h3>
<p>Edit your block here...</p>
</div>
)
}
`;
await fs.writeFile(path.join(blockDir, "edit.tsx"), content);
}
async function generateBlockRender(blockDir, name, slug, type, framework) {
if (type === "dynamic") {
const useTailwind = framework === "tailwind";
const useUno = framework === "unocss";
const classes = useTailwind || useUno ? ' class="p-4 bg-gray-100 rounded-lg"' : "";
const headingClasses = useTailwind || useUno ? ' class="text-2xl font-bold mb-2"' : "";
const content = `
/**
* ${name} Block Render
*
* @param array $attributes Block attributes
* @param string $content Block content
* @return string Rendered block
*/
$block_wrapper_attributes = get_block_wrapper_attributes();
<div echo $block_wrapper_attributes; ${classes}>
<h3${headingClasses}> echo esc_html__( '${name}', 'wp-forge' ); </h3>
<!-- Add your dynamic content here -->
</div>
`;
await fs.writeFile(path.join(blockDir, "render.php"), content);
}
}
async function generateBlockStyles(blockDir, slug, framework) {
let content;
if (framework === "tailwind" || framework === "unocss") {
content = `/*
* ${framework === "tailwind" ? "Tailwind CSS" : "UnoCSS"} utilities are used inline.
* Add custom styles here only if needed.
*/
.wp-block-wp-forge-${slug} {
/* Custom styles */
}
`;
} else {
content = `.wp-block-wp-forge-${slug} {
padding: 1rem;
background-color: #f3f4f6;
border-radius: 0.5rem;
}
.wp-block-wp-forge-${slug} h3 {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
`;
}
await fs.writeFile(path.join(blockDir, "style.css"), content);
}
// src/commands/component.ts
import path3 from "path";
import chalk5 from "chalk";
// src/utils/validation.ts
import validateNpmPackageName from "validate-npm-package-name";
function slugify(text) {
return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/^-+|-+$/g, "");
}
function toPascalCase(text) {
return text.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : "").replace(/^(.)/, (char) => char.toUpperCase());
}
function validateComponentName(name) {
if (!name || name.trim().length === 0) {
return { valid: false, error: "Component name cannot be empty" };
}
const pascal = toPascalCase(name);
if (!/^[A-Z][a-zA-Z0-9]*$/.test(pascal)) {
return { valid: false, error: "Component name must be valid PascalCase" };
}
if (pascal.length > 50) {
return { valid: false, error: "Component name is too long (max 50 characters)" };
}
return { valid: true };
}
function validateTemplateName(name) {
if (!name || name.trim().length === 0) {
return { valid: false, error: "Template name cannot be empty" };
}
const slug = slugify(name);
if (slug.length === 0) {
return { valid: false, error: "Template name must contain valid characters" };
}
if (slug.length > 50) {
return { valid: false, error: "Template name is too long (max 50 characters)" };
}
return { valid: true };
}
function validatePartName(name) {
if (!name || name.trim().length === 0) {
return { valid: false, error: "Part name cannot be empty" };
}
const slug = slugify(name);
if (slug.length === 0) {
return { valid: false, error: "Part name must contain valid characters" };
}
if (slug.length > 50) {
return { valid: false, error: "Part name is too long (max 50 characters)" };
}
return { valid: true };
}
function validateNamespace(namespace) {
if (!namespace || namespace.trim().length === 0) {
return { valid: false, error: "Namespace cannot be empty" };
}
if (!/^[A-Z][a-zA-Z0-9]*(\\[A-Z][a-zA-Z0-9]*)*$/.test(namespace)) {
return { valid: false, error: "Invalid PHP namespace format (e.g., MyTheme\\Components)" };
}
return { valid: true };
}
// src/utils/templates.ts
function generateTemplateHTML(type, slug) {
const templates = {
page: `<!-- wp:template-part {"slug":"header","theme":"${slug}","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:post-title /-->
<!-- wp:post-content /-->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","theme":"${slug}","tagName":"footer"} /-->`,
single: `<!-- wp:template-part {"slug":"header","theme":"${slug}","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:post-title /-->
<!-- wp:post-featured-image /-->
<!-- wp:group {"layout":{"type":"flex","flexWrap":"nowrap"}} -->
<div class="wp-block-group">
<!-- wp:post-date /-->
<!-- wp:post-author /-->
</div>
<!-- /wp:group -->
<!-- wp:post-content /-->
<!-- wp:post-terms {"term":"category"} /-->
<!-- wp:post-terms {"term":"post_tag"} /-->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","theme":"${slug}","tagName":"footer"} /-->`,
archive: `<!-- wp:template-part {"slug":"header","theme":"${slug}","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:query-title {"type":"archive"} /-->
<!-- wp:term-description /-->
<!-- wp:query {"queryId":0,"query":{"perPage":10,"pages":0,"offset":0,"postType":"post","order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"","inherit":true}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-excerpt /-->
<!-- wp:post-date /-->
<!-- /wp:post-template -->
<!-- wp:query-pagination -->
<!-- wp:query-pagination-previous /-->
<!-- wp:query-pagination-numbers /-->
<!-- wp:query-pagination-next /-->
<!-- /wp:query-pagination -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","theme":"${slug}","tagName":"footer"} /-->`,
"404": `<!-- wp:template-part {"slug":"header","theme":"${slug}","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:heading {"level":1} -->
<h1>404 - Page Not Found</h1>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.</p>
<!-- /wp:paragraph -->
<!-- wp:search {"label":"Search","showLabel":false,"placeholder":"Search..."} /-->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","theme":"${slug}","tagName":"footer"} /-->`,
home: `<!-- wp:template-part {"slug":"header","theme":"${slug}","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:query {"queryId":0,"query":{"perPage":10,"pages":0,"offset":0,"postType":"post","order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"","inherit":true}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:post-featured-image {"isLink":true} /-->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-excerpt /-->
<!-- wp:group {"layout":{"type":"flex","flexWrap":"nowrap"}} -->
<div class="wp-block-group">
<!-- wp:post-date /-->
<!-- wp:post-author /-->
</div>
<!-- /wp:group -->
<!-- /wp:post-template -->
<!-- wp:query-pagination -->
<!-- wp:query-pagination-previous /-->
<!-- wp:query-pagination-numbers /-->
<!-- wp:query-pagination-next /-->
<!-- /wp:query-pagination -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","theme":"${slug}","tagName":"footer"} /-->`,
search: `<!-- wp:template-part {"slug":"header","theme":"${slug}","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:query-title {"type":"search"} /-->
<!-- wp:search {"label":"Search","showLabel":false} /-->
<!-- wp:query {"queryId":0,"query":{"perPage":10,"pages":0,"offset":0,"postType":"post","order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"","inherit":true}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-excerpt /-->
<!-- /wp:post-template -->
<!-- wp:query-pagination -->
<!-- wp:query-pagination-previous /-->
<!-- wp:query-pagination-numbers /-->
<!-- wp:query-pagination-next /-->
<!-- /wp:query-pagination -->
<!-- wp:query-no-results -->
<!-- wp:paragraph -->
<p>No results found. Try a different search.</p>
<!-- /wp:paragraph -->
<!-- /wp:query-no-results -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","theme":"${slug}","tagName":"footer"} /-->`,
custom: `<!-- wp:template-part {"slug":"header","theme":"${slug}","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- Add your custom content here -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","theme":"${slug}","tagName":"footer"} /-->`
};
return templates[type] || templates.custom;
}
function generatePartHTML(type) {
const parts = {
header: `<!-- wp:group {"layout":{"type":"flex","flexWrap":"nowrap","justifyContent":"space-between"}} -->
<div class="wp-block-group">
<!-- wp:site-logo /-->
<!-- wp:site-title /-->
<!-- wp:navigation /-->
</div>
<!-- /wp:group -->`,
footer: `<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">\xA9 <!-- wp:post-date {"format":"Y"} /--> All rights reserved.</p>
<!-- /wp:paragraph -->
<!-- wp:navigation {"layout":{"type":"flex","justifyContent":"center"}} /-->
</div>
<!-- /wp:group -->`,
sidebar: `<!-- wp:group {"layout":{"type":"default"}} -->
<div class="wp-block-group">
<!-- wp:heading -->
<h2>Sidebar</h2>
<!-- /wp:heading -->
<!-- wp:search {"label":"Search","showLabel":false} /-->
<!-- wp:latest-posts /-->
<!-- wp:categories /-->
</div>
<!-- /wp:group -->`,
content: `<!-- wp:post-title /-->
<!-- wp:post-content /-->`,
custom: `<!-- wp:group {"layout":{"type":"default"}} -->
<div class="wp-block-group">
<!-- Add your custom content here -->
</div>
<!-- /wp:group -->`
};
return parts[type] || parts.custom;
}
function generatePartPHP(type, slug) {
const parts = {
header: `
/**
* Header Template Part
*
* @package ${slug}
*/
<header class="site-header">
<div class="site-branding">
if ( has_custom_logo() ) {
the_custom_logo();
}
<h1 class="site-title">
<a href=" echo esc_url( home_url( '/' ) ); " rel="home">
bloginfo( 'name' );
</a>
</h1>
$description = get_bloginfo( 'description', 'display' );
if ( $description || is_customize_preview() ) :
<p class="site-description"> echo esc_html( $description ); </p>
endif;
</div>
<nav class="main-navigation">
wp_nav_menu(
array(
'theme_location' => 'primary',
'menu_class' => 'primary-menu',
'fallback_cb' => false,
)
);
</nav>
</header>`,
footer: `
/**
* Footer Template Part
*
* @package ${slug}
*/
<footer class="site-footer">
<div class="site-info">
<p>© echo esc_html( date( 'Y' ) ); bloginfo( 'name' ); . All rights reserved.</p>
</div>
if ( has_nav_menu( 'footer' ) ) {
wp_nav_menu(
array(
'theme_location' => 'footer',
'menu_class' => 'footer-menu',
'depth' => 1,
)
);
}
</footer>`,
sidebar: `
/**
* Sidebar Template Part
*
* @package ${slug}
*/
if ( ! is_active_sidebar( 'sidebar-1' ) ) {
return;
}
<aside class="widget-area">
dynamic_sidebar( 'sidebar-1' );
</aside>`,
content: `
/**
* Content Template Part
*
* @package ${slug}
*/
<article id="post- the_ID(); " post_class(); >
<header class="entry-header">
the_title( '<h1 class="entry-title">', '</h1>' );
</header>
<div class="entry-content">
the_content();
wp_link_pages(
array(
'before' => '<div class="page-links">' . esc_html__( 'Pages:', '${slug}' ),
'after' => '</div>',
)
);
</div>
<footer class="entry-footer">
// Post meta, tags, etc.
</footer>
</article>`,
custom: `
/**
* Custom Template Part
*
* @package ${slug}
*/
<!-- Add your custom PHP code here -->`
};
return parts[type] || parts.custom;
}
function generateComponentClass(name, namespace, type) {
return `
/**
* ${name} Component
*
* @package ${namespace}
*/
namespace ${namespace}\\Components;
use WPForge\\ComponentInterface;
/**
* Class ${name}
*/
class ${name} implements ComponentInterface {
/**
* Get component slug
*
* @return string
*/
public function get_slug(): string {
return '${name.toLowerCase()}';
}
/**
* Initialize component
*
* @return void
*/
public function initialize(): void {
// Add your initialization code here
add_action( 'init', array( $this, 'register' ) );
}
/**
* Register component functionality
*
* @return void
*/
public function register(): void {
// Add your registration code here
}
}
`;
}
// src/utils/filesystem.ts
import fs2 from "fs-extra";
import path2 from "path";
import chalk4 from "chalk";
import ora4 from "ora";
async function ensureDir(dirPath) {
await fs2.ensureDir(dirPath);
}
async function writeFile(filePath, content) {
await fs2.writeFile(filePath, content, "utf8");
}
async function readFile(filePath) {
return fs2.readFile(filePath, "utf8");
}
async function createFileWithSpinner(filePath, content, description) {
const spinner = ora4(description || `Creating ${path2.basename(filePath)}`).start();
try {
await ensureDir(path2.dirname(filePath));
await writeFile(filePath, content);
spinner.succeed(chalk4.green(`Created ${path2.relative(process.cwd(), filePath)}`));
} catch (error) {
spinner.fail(chalk4.red(`Failed to create ${path2.basename(filePath)}`));
throw error;
}
}
// src/commands/component.ts
async function componentCommand(name, options) {
console.log(chalk5.cyan("\n\u2692\uFE0F Creating PHP Component\n"));
const validation = validateComponentName(name);
if (!validation.valid) {
console.error(chalk5.red(`\u2716 ${validation.error}`));
process.exit(1);
}
const className = toPascalCase(name);
const cwd = process.cwd();
const componentsDir = path3.join(cwd, "inc", "Components");
const componentPath = path3.join(componentsDir, `${className}.php`);
const fs3 = await import("fs-extra");
if (await fs3.pathExists(componentPath)) {
console.error(chalk5.red(`\u2716 Component "${className}" already exists`));
process.exit(1);
}
let namespace = options.namespace;
if (!namespace) {
try {
const existingComponents = await fs3.readdir(componentsDir);
if (existingComponents.length > 0) {
const firstComponent = existingComponents.find((f) => f.endsWith(".php"));
if (firstComponent) {
const content = await fs3.readFile(path3.join(componentsDir, firstComponent), "utf8");
const namespaceMatch = content.match(/namespace\s+([^;\\]+)/);
if (namespaceMatch) {
namespace = namespaceMatch[1];
}
}
}
} catch {
}
if (!namespace) {
const themeName = toPascalCase(path3.basename(cwd));
namespace = themeName;
}
}
const namespaceValidation = validateNamespace(namespace);
if (!namespaceValidation.valid) {
console.error(chalk5.red(`\u2716 ${namespaceValidation.error}`));
process.exit(1);
}
await ensureDir(componentsDir);
const componentContent = generateComponentClass(className, namespace, options.type);
await createFileWithSpinner(
componentPath,
componentContent,
`Creating ${className}.php component`
);
console.log(chalk5.green("\n\u2713 Component created successfully!\n"));
console.log(chalk5.dim(` Component: inc/Components/${className}.php`));
console.log(chalk5.dim(` Namespace: ${namespace}\\Components`));
console.log(chalk5.dim(` Type: ${options.type}`));
console.log(chalk5.cyan("\n Next steps:"));
console.log(chalk5.dim(" 1. Add your component logic to the class"));
console.log(chalk5.dim(" 2. Register in functions.php:"));
console.log(chalk5.dim(` new ${namespace}\\Components\\${className}(),
`));
}
// src/commands/test.ts
import chalk6 from "chalk";
async function testCommand(options) {
console.log(chalk6.cyan("\u{1F9EA} Running tests..."));
console.log(chalk6.yellow("\u26A0\uFE0F Coming soon!"));
}
// src/commands/template.ts
import path4 from "path";
import chalk7 from "chalk";
async function templateCommand(name, options) {
console.log(chalk7.cyan("\n\u2692\uFE0F Creating WordPress Template\n"));
const validation = validateTemplateName(name);
if (!validation.valid) {
console.error(chalk7.red(`\u2716 ${validation.error}`));
process.exit(1);
}
const slug = slugify(name);
const cwd = process.cwd();
const templatesDir = path4.join(cwd, "templates");
const templatePath = path4.join(templatesDir, `${slug}.html`);
const fs3 = await import("fs-extra");
if (await fs3.pathExists(templatePath)) {
console.error(chalk7.red(`\u2716 Template "${slug}" already exists`));
process.exit(1);
}
await ensureDir(templatesDir);
let themeSlug = path4.basename(cwd);
try {
const packageJson = await fs3.readJson(path4.join(cwd, "package.json"));
if (packageJson.name) {
themeSlug = packageJson.name;
}
} catch {
}
const templateContent = generateTemplateHTML(options.type, themeSlug);
await createFileWithSpinner(
templatePath,
templateContent,
`Creating ${slug}.html template`
);
console.log(chalk7.green("\n\u2713 Template created successfully!\n"));
console.log(chalk7.dim(` Template: templates/${slug}.html`));
console.log(chalk7.dim(` Type: ${options.type}`));
if (options.description) {
console.log(chalk7.dim(` Description: ${options.description}`));
}
console.log(chalk7.cyan("\n Next steps:"));
console.log(chalk7.dim(" 1. Edit the template in templates/" + slug + ".html"));
console.log(chalk7.dim(" 2. Add your custom blocks and content"));
console.log(chalk7.dim(" 3. The template will be available in WordPress theme editor\n"));
}
// src/commands/part.ts
import path5 from "path";
import chalk8 from "chalk";
async function partCommand(name, options) {
console.log(chalk8.cyan("\n\u2692\uFE0F Creating Template Part\n"));
const validation = validatePartName(name);
if (!validation.valid) {
console.error(chalk8.red(`\u2716 ${validation.error}`));
process.exit(1);
}
const slug = slugify(name);
const cwd = process.cwd();
const partsDir = path5.join(cwd, "parts");
const ext = options.markup === "html" ? "html" : "php";
const partPath = path5.join(partsDir, `${slug}.${ext}`);
const fs3 = await import("fs-extra");
if (await fs3.pathExists(partPath)) {
console.error(chalk8.red(`\u2716 Template part "${slug}.${ext}" already exists`));
process.exit(1);
}
await ensureDir(partsDir);
let themeSlug = path5.basename(cwd);
try {
const packageJson = await fs3.readJson(path5.join(cwd, "package.json"));
if (packageJson.name) {
themeSlug = packageJson.name;
}
} catch {
}
const partContent = options.markup === "html" ? generatePartHTML(options.type) : generatePartPHP(options.type, themeSlug);
await createFileWithSpinner(
partPath,
partContent,
`Creating ${slug}.${ext} template part`
);
console.log(chalk8.green("\n\u2713 Template part created successfully!\n"));
console.log(chalk8.dim(` Part: parts/${slug}.${ext}`));
console.log(chalk8.dim(` Type: ${options.type}`));
console.log(chalk8.dim(` Markup: ${options.markup}`));
console.log(chalk8.cyan("\n Next steps:"));
console.log(chalk8.dim(` 1. Edit the part in parts/${slug}.${ext}`));
console.log(chalk8.dim(' 2. Use in templates with <!-- wp:template-part {"slug":"' + slug + '"} /-->'));
console.log(chalk8.dim(" 3. Or in PHP with get_template_part('parts/" + slug + "')\n"));
}
// src/commands/design-system.ts
import path6 from "path";
import chalk9 from "chalk";
import ora5 from "ora";
import { execa as execa3 } from "execa";
async function designSystemCommand(framework) {
console.log(chalk9.cyan("\n\u2692\uFE0F Setting up Design System\n"));
if (framework !== "tailwind" && framework !== "unocss") {
console.error(chalk9.red("\u2716 Invalid framework. Choose: tailwind or unocss"));
process.exit(1);
}
const cwd = process.cwd();
const packageManager = await detectPackageManager();
await installDependencies(framework, packageManager);
await createConfigFile(framework, cwd);
await updateViteConfig(framework, cwd);
await createCSSEntry(framework, cwd);
console.log(chalk9.green("\n\u2713 Design system setup complete!\n"));
console.log(chalk9.cyan(" Next steps:"));
console.log(chalk9.dim(` 1. Import the CSS in your main JS file:`));
console.log(chalk9.dim(` import '../css/design-system.css'`));
console.log(chalk9.dim(` 2. Start using utility classes in your blocks`));
console.log(chalk9.dim(` 3. Customize ${framework === "tailwind" ? "tailwind" : "uno"}.config.${framework === "tailwind" ? "js" : "ts"} as needed
`));
}
async function detectPackageManager() {
const fs3 = await import("fs-extra");
const cwd = process.cwd();
if (await fs3.pathExists(path6.join(cwd, "pnpm-lock.yaml"))) {
return "pnpm";
}
if (await fs3.pathExists(path6.join(cwd, "yarn.lock"))) {
return "yarn";
}
return "npm";
}
async function installDependencies(framework, pm) {
const spinner = ora5("Installing dependencies...").start();
try {
const packages = framework === "tailwind" ? ["tailwindcss", "postcss", "autoprefixer"] : ["@unocss/vite", "unocss"];
const installCmd = pm === "yarn" ? "add" : "install";
const devFlag = pm === "yarn" ? "--dev" : "--save-dev";
await execa3(pm, [installCmd, devFlag, ...packages], {
cwd: process.cwd(),
stdio: "pipe"
});
spinner.succeed(chalk9.green("Dependencies installed"));
} catch (error) {
spinner.fail("Failed to install dependencies");
console.error(error);
process.exit(1);
}
}
async function createConfigFile(framework, cwd) {
if (framework === "tailwind") {
const configContent = `import { wpForgeTailwindPreset } from '@wp-forge/vite-plugin/integrations/tailwind-preset'
export default {
presets: [wpForgeTailwindPreset],
theme: {
extend: {
// Add your custom theme extensions here
},
},
}
`;
await createFileWithSpinner(
path6.join(cwd, "tailwind.config.js"),
configContent,
"Creating tailwind.config.js"
);
} else {
const configContent = `import { defineConfig, presetUno } from 'unocss'
import { wpForgeUnoPreset } from '@wp-forge/vite-plugin/integrations/unocss-preset'
export default defineConfig({
presets: [
presetUno(),
wpForgeUnoPreset,
],
theme: {
// Add your custom theme here
},
})
`;
await createFileWithSpinner(
path6.join(cwd, "uno.config.ts"),
configContent,
"Creating uno.config.ts"
);
}
}
async function updateViteConfig(framework, cwd) {
const spinner = ora5("Updating vite.config.ts...").start();
const viteConfigPath = path6.join(cwd, "vite.config.ts");
try {
let config = await readFile(viteConfigPath);
if (framework === "unocss" && !config.includes("unocss/vite")) {
config = `import UnoCSS from '@unocss/vite'
${config}`;
}
if (config.includes("wpForge({")) {
const designSystemConfig = `
designSystem: {
enabled: true,
framework: '${framework}',
wordpressPresets: true,
},`;
config = config.replace(/(\s+}[\s\n]*\)[\s\n]*,?[\s\n]*plugins:)/, `${designSystemConfig}$1`);
if (framework === "unocss" && !config.includes("UnoCSS()")) {
config = config.replace(/(plugins:\s*\[)/, "$1\n UnoCSS(),");
}
}
await writeFile(viteConfigPath, config);
spinner.succeed(chalk9.green("Updated vite.config.ts"));
} catch (error) {
spinner.fail("Failed to update vite.config.ts");
console.warn(chalk9.yellow(" Please manually update your vite.config.ts"));
}
}
async function createCSSEntry(framework, cwd) {
const cssDir = path6.join(cwd, "src", "css");
const cssPath = path6.join(cssDir, "design-system.css");
let content;
if (framework === "tailwind") {
content = `/**
* Tailwind CSS Entry
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom component styles */
@layer components {
.btn {
@apply px-4 py-2 rounded bg-wp-primary text-white hover:bg-wp-secondary transition;
}
}
`;
} else {
content = `/**
* UnoCSS Entry
*
* UnoCSS generates styles on-demand, so this file mainly contains
* custom CSS that isn't covered by utility classes.
*/
/* Custom component styles */
.btn {
padding: 1rem 1.5rem;
border-radius: 0.25rem;
background-color: var(--wp--preset--color--primary);
color: white;
transition: background-color 0.2s;
}
.btn:hover {
background-color: var(--wp--preset--color--secondary);
}
`;
}
await createFileWithSpinner(cssPath, content, "Creating design-system.css");
}
// src/index.ts
var program = new Command();
program.name("wp-forge").description("\u2692\uFE0F A modern WordPress theme framework").version("0.2.2");
program.command("dev").description("Start development server with hot reload").option("-p, --port <port>", "Port number", "3000").option("-h, --host <host>", "Host address", "localhost").action(devCommand);
program.command("build").description("Build theme for production").option("--analyze", "Analyze bundle size").action(buildCommand);
program.command("block:new <name>").description("Create a new Gutenberg block").option("-t, --type <type>", "Block type (static|dynamic)", "dynamic").option("--category <category>", "Block category", "common").option("--styleFramework <framework>", "Style framework (none|tailwind|unocss)", "none").action(blockCommand);
program.command("component:new <name>").description("Create a new theme component").option("-t, --type <type>", "Component type (service|feature|integration|custom)", "custom").option("--namespace <namespace>", "PHP namespace").action(componentCommand);
program.command("template:new <name>").description("Create a new WordPress template").option("-t, --type <type>", "Template type (page|single|archive|404|home|search|custom)", "page").option("--description <text>", "Template description").action(templateCommand);
program.command("part:new <name>").description("Create a new template part").option("-t, --type <type>", "Part type (header|footer|sidebar|content|custom)", "custom").option("--markup <markup>", "Markup style (html|php)", "php").action(partCommand);
program.command("design-system:setup <framework>").description("Set up a design system (tailwind|unocss)").action(designSystemCommand);
program.command("test").description("Run tests").option("--unit", "Run unit tests only").option("--e2e", "Run E2E tests only").option("--watch", "Watch mode").action(testCommand);
program.command("storybook").description("Launch component explorer").action(() => {
console.log(chalk10.cyan("\u{1F3AD} Launching component explorer..."));
console.log(chalk10.dim("Coming soon!"));
});
program.command("ai:block <description>").description("Generate block from description using AI").action((description) => {
console.log(chalk10.magenta("\u{1F916} AI Block Generator"));
console.log(chalk10.dim(`Description: ${description}`));
console.log(chalk10.yellow("\u26A0\uFE0F Coming soon!"));
});
program.command("ai:optimize").description("AI-powered performance optimization").action(() => {
console.log(chalk10.magenta("\u{1F916} AI Optimizer"));
console.log(chalk10.yellow("\u26A0\uFE0F Coming soon!"));
});
program.parse();