@haxtheweb/create
Version:
CLI for all things HAX the web
1,775 lines • 75.8 kB
JavaScript
#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.siteActions = siteActions;
exports.siteCommandDetected = siteCommandDetected;
exports.siteItemsOptionsList = siteItemsOptionsList;
exports.siteNodeOperations = siteNodeOperations;
exports.siteNodeStatsOperations = siteNodeStatsOperations;
exports.siteProcess = siteProcess;
exports.siteThemeList = siteThemeList;
var _promises = require("node:timers/promises");
var fs = _interopRequireWildcard(require("node:fs"));
var path = _interopRequireWildcard(require("node:path"));
var _open = _interopRequireDefault(require("open"));
var p = _interopRequireWildcard(require("@clack/prompts"));
var ejs = _interopRequireWildcard(require("ejs"));
var _picocolors = _interopRequireDefault(require("picocolors"));
var _jsYaml = require("js-yaml");
var winston = _interopRequireWildcard(require("winston"));
var _nodeHtmlParser = require("node-html-parser");
var _statements = require("../statements.js");
var _utils = require("../utils.js");
var _logging = require("../logging.js");
var _microFrontendRegistry = require("../micro-frontend-registry.js");
var haxcmsNodejsCli = _interopRequireWildcard(require("@haxtheweb/haxcms-nodejs/dist/cli.js"));
var hax = _interopRequireWildcard(require("@haxtheweb/haxcms-nodejs"));
var josfile = _interopRequireWildcard(require("@haxtheweb/haxcms-nodejs/dist/lib/JSONOutlineSchema.js"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
// trick MFR into giving local paths
globalThis.MicroFrontendRegistryConfig = {
base: `@haxtheweb/open-apis`
};
// emable HAXcms routes so we have name => path just like on frontend!
_microFrontendRegistry.MicroFrontendRegistry.enableServices(['core', 'haxcms', 'experimental']);
const JSONOutlineSchema = josfile.default;
const HAXCMS = hax.HAXCMS;
var sysSurge = true;
(0, _utils.exec)('surge --version', error => {
if (error) {
sysSurge = false;
}
});
const siteRecipeFile = 'create-cli.recipe';
const siteLoggingName = 'cli';
const logLevels = {};
logLevels[siteLoggingName] = 0;
// fake response class so we can capture the response from the headless route as opposed to print to console
// this way we can handle as data or if use is requesting output format to change we can respond
class Res {
constructor() {
this.query = {};
this.data = null;
this.statusCode = null;
}
send(data) {
this.data = data;
return this;
}
status(status) {
this.statusCode = status;
return this;
}
json(data) {
this.data = JSON.parse(JSON.stringify(data));
return this;
}
setHeader() {
return this;
}
}
function siteActions() {
return [{
value: 'start',
label: "Launch site in browser (http://localhost)"
}, {
value: 'serve',
label: "Launch site in development mode"
}, {
value: 'node:stats',
label: "Node Stats / data"
}, {
value: 'node:add',
label: "Add a new page"
}, {
value: 'node:edit',
label: "Edit a page"
}, {
value: 'node:delete',
label: "Delete a page"
}, {
value: 'site:stats',
label: "Site Status / stats"
}, {
value: 'site:items',
label: "Site items"
}, {
value: 'site:items-import',
label: "Import items (JOS / site.json)"
}, {
value: 'site:list-files',
label: "List site files"
}, {
value: 'site:theme',
label: "Change theme"
}, {
value: 'site:element',
label: "Add new Lit component to custom/src"
}, {
value: 'site:html',
label: "Full site as HTML"
}, {
value: 'site:md',
label: "Full site as Markdown"
}, {
value: 'site:schema',
label: "Full site as HAXElementSchema"
}, {
value: 'site:sync',
label: "Sync git repo"
}, {
value: 'site:surge',
label: "Publish site to Surge.sh"
}, {
value: 'recipe:read',
label: "Read recipe file"
}, {
value: 'recipe:play',
label: "Play recipe file"
}, {
value: 'issue:general',
label: "Issue: Submit an issue or suggestion"
}, {
value: 'issue:theme',
label: "Issue: Suggest custom theme"
}];
}
async function siteCommandDetected(commandRun) {
var activeHaxsite = await hax.systemStructureContext();
const recipeFileName = path.join(process.cwd(), siteRecipeFile);
const recipeLogTransport = new winston.transports.File({
filename: recipeFileName
});
const recipe = winston.createLogger({
levels: logLevels,
level: siteLoggingName,
transports: [recipeLogTransport],
format: winston.format.simple()
});
let actionAssigned = false;
// default to status unless already set so we don't issue a create in a create
if (!commandRun.arguments.action) {
actionAssigned = true;
commandRun.arguments.action = 'site:status';
}
commandRun.command = "site";
if (!commandRun.options.y && commandRun.options.i && !commandRun.options.quiet) {
p.intro(`${_picocolors.default.bgBlack(_picocolors.default.white(` HAXTheWeb : Site detected `))}`);
p.intro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Name: ${activeHaxsite.name} `))}`);
}
// defaults if nothing set via CLI
let operation = {
...commandRun.arguments,
...commandRun.options
};
if (!commandRun.options.title) {
commandRun.options.title = "New Page";
}
if (!commandRun.options.domain && commandRun.options.y) {
commandRun.options.domain = `haxcli-${activeHaxsite.name}.surge.sh`;
}
// infinite loop until quitting the cli
while (operation.action !== 'quit') {
let actions = siteActions();
actions.push({
value: 'quit',
label: "🚪 Quit"
});
if (!operation.action) {
commandRun = {
command: null,
arguments: {},
options: {}
};
// ensures data is updated and stateful per action
activeHaxsite = await hax.systemStructureContext();
operation = await p.group({
action: ({
results
}) => p.select({
message: `Actions you can take`,
options: actions
})
}, {
onCancel: () => {
if (!commandRun.options.quiet) {
p.cancel('🧙 Merlin: Canceling CLI.. HAX ya later 🪄');
(0, _statements.communityStatement)();
}
process.exit(0);
}
});
}
if (operation.action) {
p.intro(`hax site ${_picocolors.default.bold(operation.action)}`);
}
switch (operation.action) {
case "site:status": // easy mistype
case "site:stats":
const date = new Date(activeHaxsite.manifest.metadata.site.updated * 1000);
let siteItems = [];
if (commandRun.options.itemId != null) {
siteItems = activeHaxsite.manifest.findBranch(commandRun.options.itemId);
} else {
siteItems = activeHaxsite.manifest.orderTree(activeHaxsite.manifest.items);
}
let els = {};
for (var i in siteItems) {
let page = activeHaxsite.loadNode(siteItems[i].id);
let html = await activeHaxsite.getPageContent(page);
let dom = (0, _nodeHtmlParser.parse)(`<div id="fullpage">${html}</div>`);
for (var j in dom.querySelector('#fullpage').childNodes) {
let node = dom.querySelector('#fullpage').childNodes[j];
if (node && node.getAttribute) {
let haxel = await nodeToHaxElement(node, null);
if (!els[haxel.tag]) {
els[haxel.tag] = 0;
}
els[haxel.tag]++;
}
}
}
let siteStats = {
title: activeHaxsite.manifest.title,
description: activeHaxsite.manifest.description,
themeName: activeHaxsite.manifest.metadata.theme.name,
themeElement: activeHaxsite.manifest.metadata.theme.element,
pageCount: activeHaxsite.manifest.items.length,
lastUpdated: date.toLocaleDateString("en-US"),
tagUsage: els
};
if (!commandRun.options.format && !commandRun.options.quiet) {
p.intro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Title: ${siteStats.title} `))}`);
p.intro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Description: ${siteStats.description} `))}`);
p.intro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Theme: ${siteStats.themeName} (${siteStats.themeElement})`))}`);
p.intro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Pages: ${siteStats.pageCount} `))}`);
p.intro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Last updated: ${siteStats.lastUpdated} `))}`);
p.intro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Tags used: ${JSON.stringify(siteStats.tagUsage, null, 2)} `))}`);
} else if (commandRun.options.format === 'yaml') {
(0, _logging.log)((0, _jsYaml.dump)(siteStats));
} else {
(0, _logging.log)(siteStats);
}
// simple redirecting to file
if (commandRun.options.toFile) {
if (commandRun.options.format === 'yaml') {
fs.writeFileSync(commandRun.options.toFile, (0, _jsYaml.dump)(siteStats));
} else {
fs.writeFileSync(commandRun.options.toFile, JSON.stringify(siteStats, null, 2));
}
}
break;
case "site:items":
let siteitems = [];
if (commandRun.options.itemId != null) {
siteitems = activeHaxsite.manifest.findBranch(commandRun.options.itemId);
} else {
siteitems = activeHaxsite.manifest.orderTree(activeHaxsite.manifest.items);
}
for (let i in siteitems) {
let page = await activeHaxsite.loadNode(siteitems[i].id);
siteitems[i].content = await activeHaxsite.getPageContent(page);
}
// simple redirecting to file if asked for
if (commandRun.options.toFile) {
let contents = '';
if (commandRun.options.format === 'yaml') {
contents = (0, _jsYaml.dump)(siteitems);
} else {
contents = JSON.stringify(siteitems, null, 2);
}
fs.writeFileSync(commandRun.options.toFile, contents);
} else {
if (commandRun.options.format === 'yaml') {
(0, _logging.log)((0, _jsYaml.dump)(siteitems));
} else {
(0, _logging.log)(siteitems);
}
}
break;
case "site:items-import":
// need source, then resolve what it is
if (commandRun.options.itemsImport) {
let location = commandRun.options.itemsImport;
let josImport = new JSONOutlineSchema();
var itemsImport = [];
// support for address, as in import from some place else
if (location.startsWith('https://') || location.startsWith('http://')) {
if (location.endsWith('/site.json')) {
location = location.replace('/site.json', '');
} else if (!location.endsWith('/')) {
location = location + '/';
}
let f = await fetch(`${location}site.json`).then(d => d.ok ? d.json() : null);
if (f && f.items) {
josImport.items = f.items;
} else {
// invalid data
process.exit(0);
}
}
// look on prem
else if (fs.existsSync(location)) {
let fileContents = await fs.readFileSync(location);
if (location.endsWith('.json')) {
josImport.items = JSON.parse(fileContents);
} else if (location.endsWith('.yaml')) {
josImport.items = await (0, _jsYaml.load)(fileContents);
}
}
// allows for filtering
if (commandRun.options.itemId) {
itemsImport = josImport.findBranch(commandRun.options.itemId);
} else {
itemsImport = josImport.items;
}
for (let i in josImport.items) {
if (josImport.items[i].location && !josImport.items[i].content) {
josImport.items[i].content = await fetch(`${location}${josImport.items[i].location}`).then(d => d.ok ? d.text() : '');
}
}
let itemIdMap = {};
for (let i in josImport.items) {
// if we have a parent set by force to append this structure to
// then see if parent = null (implying top level in full site import)
// or match on itemId to imply that it's the top (no matter parent status)
if (commandRun.options.parentId) {
if (josImport.items[i].parent === null || josImport.items[i].id === commandRun.options.itemId) {
josImport.items[i].parent = commandRun.options.parentId;
}
}
// see if map has an entry that is already set
if (itemIdMap[josImport.items[i].parent]) {
// remaps the parent of this item bc the thing imported has changed ID
josImport.items[i].parent = itemIdMap[josImport.items[i].parent];
}
let tmpAddedItem = await activeHaxsite.addPage(josImport.items[i].parent, josImport.items[i].title, 'html', josImport.items[i].slug, null, josImport.items[i].indent, josImport.items[i].content, josImport.items[i].order, josImport.items[i].metadata);
// set in the map for future translations
itemIdMap[josImport.items[i].id] = tmpAddedItem.id;
}
if (!commandRun.options.quiet) {
(0, _logging.log)(`${josImport.items.length} nodes imported`);
}
recipe.log(siteLoggingName, (0, _logging.commandString)(commandRun));
} else if (!commandRun.options.quiet) {
(0, _logging.log)('Must specify --items-import as path to valid item export file or URL', 'error');
}
break;
case "start":
try {
if (!commandRun.options.quiet) {
p.intro(`Starting server.. `);
p.note(`🚀 Server running at: ${_picocolors.default.underline(_picocolors.default.cyan(`http://localhost:3000`))}
⌨️ To stop server, press: ${_picocolors.default.bold(_picocolors.default.black(_picocolors.default.bgRed(` CTRL + C or CTRL + BREAK `)))}`);
}
await (0, _utils.exec)(`cd ${activeHaxsite.directory} && npx @haxtheweb/haxcms-nodejs`);
} catch (e) {
(0, _logging.log)(e.stderr);
}
break;
case "serve":
try {
if (!commandRun.options.quiet) {
p.intro(`Starting server in development mode.. `);
p.note(`🚀 Server running at: ${_picocolors.default.underline(_picocolors.default.cyan(`http://localhost:3000`))}
💻 Site will live reload on changes to ${_picocolors.default.bold('custom/src')}
⌨️ To stop server, press: ${_picocolors.default.bold(_picocolors.default.black(_picocolors.default.bgRed(` CTRL + C or CTRL + BREAK `)))}`);
}
await (0, _utils.exec)(`cd ${activeHaxsite.directory} && NODE_ENV=development npx @haxtheweb/haxcms-nodejs`);
} catch (e) {
(0, _logging.log)(e.stderr);
}
break;
case "node:status": // easy mistype
case "node:stats":
try {
if (!commandRun.options.itemId) {
commandRun.options.itemId = await p.select({
message: `Select an item to edit`,
required: true,
options: [{
value: null,
label: "-- edit nothing, exit --"
}, ...(await siteItemsOptionsList(activeHaxsite))]
});
}
if (commandRun.options.itemId) {
let nodeOps = siteNodeStatsOperations();
let page = activeHaxsite.loadNode(commandRun.options.itemId);
// select which aspect of this we are editing
if (!commandRun.options.nodeOp) {
commandRun.options.nodeOp = await p.select({
message: `${page.title} (${page.id}) - Node operations`,
required: true,
options: [{
value: null,
label: "-- Exit --"
}, ...nodeOps]
});
}
if (commandRun.options.nodeOp && siteNodeStatsOperations(commandRun.options.nodeOp)) {
switch (commandRun.options.nodeOp) {
case 'details':
if (commandRun.options.format === 'yaml') {
(0, _logging.log)((0, _jsYaml.dump)(page));
} else {
(0, _logging.log)(page);
}
// simple redirecting to file
if (commandRun.options.toFile) {
if (commandRun.options.format === 'yaml') {
fs.writeFileSync(commandRun.options.toFile, (0, _jsYaml.dump)(page));
} else {
fs.writeFileSync(commandRun.options.toFile, JSON.stringify(page, null, 2));
}
}
break;
case 'html':
let itemHTML = await activeHaxsite.getPageContent(page);
// simple redirecting to file
if (commandRun.options.toFile) {
fs.writeFileSync(commandRun.options.toFile, itemHTML);
} else {
(0, _logging.log)(itemHTML);
}
break;
case 'schema':
// next up
let html = await activeHaxsite.getPageContent(page);
let dom = (0, _nodeHtmlParser.parse)(`<div id="fullpage">${html}</div>`);
let els = [];
for (var i in dom.querySelector('#fullpage').childNodes) {
let node = dom.querySelector('#fullpage').childNodes[i];
if (node && node.getAttribute) {
els.push(await nodeToHaxElement(node, null));
}
}
// simple redirecting to file
if (commandRun.options.toFile) {
if (commandRun.options.format === 'yaml') {
fs.writeFileSync(commandRun.options.toFile, (0, _jsYaml.dump)(els));
} else {
fs.writeFileSync(commandRun.options.toFile, JSON.stringify(els, null, 2));
}
} else {
if (commandRun.options.format === 'yaml') {
(0, _logging.log)((0, _jsYaml.dump)(els));
} else {
(0, _logging.log)(els);
}
}
break;
case 'md':
let resp = await openApiBroker('@core', 'htmlToMd', {
html: await activeHaxsite.getPageContent(page)
});
// simple redirecting to file
if (commandRun.options.toFile) {
fs.writeFileSync(commandRun.options.toFile, resp.res.data.data);
} else {
(0, _logging.log)(resp.res.data.data);
}
break;
}
}
}
} catch (e) {
(0, _logging.log)(e.stderr);
(0, _logging.log)(e);
}
break;
case "node:add":
try {
if (!commandRun.options.title) {
commandRun.options.title = await p.text({
message: `Title for this page`,
placeholder: "New page",
required: true,
validate: value => {
if (!value) {
return "Title must be set (tab writes default)";
}
}
});
}
var createNodeBody = {
site: activeHaxsite,
node: {
title: commandRun.options.title
}
};
// this would be odd but could be direct with no format specified
if (commandRun.options.content && !commandRun.options.format) {
// only API where it's called contents and already out there {facepalm}
// but user already has commands where it's --content as arg
createNodeBody.node.contents = commandRun.options.content;
} else if (commandRun.options.content && commandRun.options.format) {
let locationContent = '';
// if we have format set, then we need to interpret content as a url
let location = commandRun.options.content;
// support for address, as in import from some place else
if (location.startsWith('https://') || location.startsWith('http://')) {
locationContent = await fetch(location).then(d => d.ok ? d.text() : '');
}
// look on prem
else if (fs.existsSync(location)) {
locationContent = await fs.readFileSync(location);
}
// format dictates additional processing; html is default
switch (commandRun.options.format) {
case 'json':
locationContent = JSON.parse(locationContent);
break;
case 'yaml':
locationContent = await (0, _jsYaml.load)(locationContent);
break;
case 'md':
let resp = await openApiBroker('@core', 'mdToHtml', {
md: locationContent,
raw: true
});
if (resp.res.data) {
locationContent = resp.res.data;
}
break;
}
// support for scraper mode to find title from the content responsee
if (commandRun.options.titleScrape) {
let dom = (0, _nodeHtmlParser.parse)(`${locationContent}`);
createNodeBody.node.title = dom.querySelector(`${commandRun.options.titleScrape}`).textContent;
}
// support scraper mode which targets a wrapper for the actual content
if (commandRun.options.contentScrape) {
let dom = (0, _nodeHtmlParser.parse)(`${locationContent}`);
locationContent = dom.querySelector(`${commandRun.options.contentScrape}`).innerHTML;
}
createNodeBody.node.contents = locationContent;
}
let resp = await haxcmsNodejsCli.cliBridge('createNode', createNodeBody);
recipe.log(siteLoggingName, (0, _logging.commandString)(commandRun));
if (commandRun.options.v) {
(0, _logging.log)(resp.res.data, 'silly');
}
if (!commandRun.options.quiet) {
(0, _logging.log)(`"${createNodeBody.node.title}" added to site`, 'info', createNodeBody.node);
}
} catch (e) {
(0, _logging.log)(e.stderr);
}
break;
case "node:edit":
try {
if (!commandRun.options.itemId) {
commandRun.options.itemId = await p.select({
message: `Select an item to edit`,
required: true,
options: [{
value: null,
label: "-- edit nothing, exit --"
}, ...(await siteItemsOptionsList(activeHaxsite))]
});
}
if (commandRun.options.itemId) {
let nodeOps = siteNodeOperations();
let page = activeHaxsite.loadNode(commandRun.options.itemId);
// select which aspect of this we are editing
if (!commandRun.options.nodeOp) {
commandRun.options.nodeOp = await p.select({
message: `${page.title} (${page.id}) - Node operations`,
required: true,
options: [{
value: null,
label: "-- Exit --"
}, ...nodeOps]
});
}
if (commandRun.options.nodeOp && siteNodeOperations(commandRun.options.nodeOp)) {
let nodeProp = commandRun.options.nodeOp;
var propValue = commandRun.options[nodeProp];
// verify we have a setting for the operation requested
// otherwise we get interactive again
if (!commandRun.options[nodeProp]) {
let val = page[nodeProp];
if (['tags', 'published', 'hideInMenu', 'theme'].includes(nodeProp)) {
val = page.metadata[nodeProp];
} else if (nodeProp === 'content') {
val = await activeHaxsite.getPageContent(page);
}
// boolean is confirm
if (['published', 'hideInMenu'].includes(nodeProp)) {
propValue = await p.confirm({
message: `${nodeProp}:`,
initialValue: Boolean(val),
defaultValue: Boolean(val)
});
}
// these have fixed possible values
else if (['parent', 'theme'].includes(nodeProp)) {
let l = nodeProp === 'parent' ? "-- no parent --" : "-- no theme --";
let list = nodeProp === 'parent' ? await siteItemsOptionsList(activeHaxsite, page.id) : await siteThemeList(true);
propValue = await p.select({
message: `${nodeProp}:`,
defaultValue: val,
initialValue: val,
options: [{
value: null,
label: l
}, ...list]
});
} else {
propValue = await p.text({
message: `${nodeProp}:`,
initialValue: val,
defaultValue: val
});
}
}
if (nodeProp === 'order') {
propValue = parseInt(propValue);
}
// account for CLI
if (propValue === "null") {
propValue = null;
}
commandRun.options[nodeProp] = propValue;
}
// ensure we set empty values, just not completely undefined values
if (typeof commandRun.options[commandRun.options.nodeOp] !== "undefined") {
if (commandRun.options.nodeOp === 'content') {
let locationContent = '';
// this would be odd but could be direct with no format specified
if (commandRun.options.content && !commandRun.options.format) {
locationContent = commandRun.options.content;
}
// this implies what we were given needs processing as a file / url
else if (commandRun.options.content && commandRun.options.format) {
// if we have format set, then we need to interpret content as a url
let location = commandRun.options.content;
// support for address, as in import from some place else
if (location.startsWith('https://') || location.startsWith('http://')) {
locationContent = await fetch(location).then(d => d.ok ? d.text() : '');
}
// look on prem
else if (fs.existsSync(location)) {
locationContent = await fs.readFileSync(location);
}
// format dictates additional processing; html is default
switch (commandRun.options.format) {
case 'json':
locationContent = JSON.parse(locationContent);
break;
case 'yaml':
locationContent = await (0, _jsYaml.load)(locationContent);
break;
case 'md':
let resp = await openApiBroker('@core', 'mdToHtml', {
md: locationContent,
raw: true
});
if (resp.res.data) {
locationContent = resp.res.data;
}
break;
}
// support scraper mode which targets a wrapper for the actual content
if (commandRun.options.contentScrape) {
let dom = (0, _nodeHtmlParser.parse)(`${locationContent}`);
locationContent = dom.querySelector(`${commandRun.options.contentScrape}`).innerHTML;
}
}
// if we have content (meaning it's not blank) then try to write the page location
if (locationContent && (await page.writeLocation(locationContent))) {
recipe.log(siteLoggingName, (0, _logging.commandString)(commandRun));
if (!commandRun.options.quiet) {
(0, _logging.log)(`node:edit success updated page content: "${page.id}`);
}
} else {
console.warn(`node:edit failure to write page content : ${page.id}`);
}
} else {
if (['tags', 'published', 'hideInMenu'].includes(commandRun.options.nodeOp)) {
page.metadata[commandRun.options.nodeOp] = commandRun.options[commandRun.options.nodeOp];
} else if (commandRun.options.nodeOp === 'theme') {
let themes = await HAXCMS.getThemes();
page.metadata.theme = themes[commandRun.options[commandRun.options.nodeOp]];
} else {
page[commandRun.options.nodeOp] = commandRun.options[commandRun.options.nodeOp];
}
let resp = await activeHaxsite.updateNode(page);
recipe.log(siteLoggingName, (0, _logging.commandString)(commandRun));
if (commandRun.options.v) {
(0, _logging.log)(resp, 'silly');
}
}
}
}
} catch (e) {
(0, _logging.log)(e.stderr);
}
break;
case "node:delete":
try {
if (!commandRun.options.itemId) {
commandRun.options.itemId = await p.select({
message: `Select an item to delete`,
required: true,
options: [{
value: null,
label: "-- Delete nothing, exit --"
}, ...(await siteItemsOptionsList(activeHaxsite))]
});
}
if (commandRun.options.itemId) {
let del = false;
if (!commandRun.options.y) {
del = await p.confirm({
message: `Are you sure you want to delete ${commandRun.options.itemId}? (This cannot be undone)`,
initialValue: true
});
} else {
del = true;
}
// extra confirmation given destructive operation
if (del) {
let resp = await haxcmsNodejsCli.cliBridge('deleteNode', {
site: activeHaxsite,
node: {
id: commandRun.options.itemId
}
});
if (resp.res.data === 500) {
console.warn(`node:delete failed "${commandRun.options.itemId} not found`);
} else {
recipe.log(siteLoggingName, (0, _logging.commandString)(commandRun));
(0, _logging.log)(`"${commandRun.options.itemId}" deleted`);
}
} else {
(0, _logging.log)(`Delete operation canceled`);
}
}
} catch (e) {
(0, _logging.log)(e.stderr);
}
break;
case "site:sync":
// @todo git sync might need other arguments / be combined with publishing
try {
await (0, _utils.exec)(`cd ${activeHaxsite.directory} && git pull && git push`);
} catch (e) {
(0, _logging.log)(e.stderr);
}
break;
case "site:theme":
try {
//theme
activeHaxsite = await hax.systemStructureContext();
let list = await siteThemeList(true, activeHaxsite.directory);
let val = activeHaxsite.manifest.metadata.theme.element;
if (!commandRun.options.theme) {
commandRun.options.theme = await p.select({
message: `Select theme:`,
defaultValue: val,
initialValue: val,
options: list
});
}
if (commandRun.options.theme === "custom-theme") {
if (!commandRun.options.customThemeName) {
commandRun.options.customThemeName = await p.text({
message: 'Theme Name:',
placeholder: `custom-${activeHaxsite.name}-theme`,
required: false,
validate: value => {
if (!value) {
return "Theme name is required (tab writes default)";
}
if (list.some(theme => theme.value === value)) {
return "Theme name is already in use";
}
if (/^\d/.test(value)) {
return "Theme name cannot start with a number";
}
if (/[A-Z]/.test(value)) {
return "No uppercase letters allowed in theme name";
}
if (value.indexOf(' ') !== -1) {
return "No spaces allowed in theme name";
}
}
});
}
if (!commandRun.options.customThemeTemplate) {
const options = [{
value: 'base',
label: 'Vanilla Theme with Hearty Documentation'
}, {
value: 'polaris-flex',
label: 'Minimalist Theme with Horizontal Nav'
}, {
value: 'polaris-sidebar',
label: 'Content-Focused Theme with Flexible Sidebar'
}];
commandRun.options.customThemeTemplate = await p.select({
message: 'Template:',
required: false,
options: options,
initialValue: options[0]
});
}
}
let themes = await HAXCMS.getThemes();
if (themes && commandRun.options.theme) {
if (themes[commandRun.options.theme]) {
activeHaxsite.manifest.metadata.theme = themes[commandRun.options.theme];
activeHaxsite.manifest.save(false);
recipe.log(siteLoggingName, (0, _logging.commandString)(commandRun));
} else if (commandRun.options.theme === "custom-theme") {
commandRun.options.name = activeHaxsite.name;
commandRun.options.directory = activeHaxsite.directory;
// temporary for proof of concept
commandRun.options.npmClient = 'npm';
await customSiteTheme(commandRun, {});
} else if (!themes[commandRun.options.theme]) {
let themeObj = {
element: commandRun.options.theme,
path: "./custom/build/custom.es6.js",
name: (0, _utils.dashToCamel)(commandRun.options.theme)
};
activeHaxsite.manifest.metadata.theme = themeObj;
activeHaxsite.manifest.save(false);
}
}
} catch (e) {
(0, _logging.log)(e.stderr);
}
break;
case "site:element":
try {
const reservedNames = ["annotation-xml", "color-profile", "font-face", "font-face-src", "font-face-uri", "font-face-format", "font-face-name", "missing-glyph"];
activeHaxsite = await hax.systemStructureContext();
if (!commandRun.options.name) {
commandRun.options.name = await p.text({
message: 'Component name:',
placeholder: 'my-component',
required: true,
validate: value => {
if (!value) {
return "Name is required (tab writes default)";
}
if (reservedNames.includes(value)) {
return `Reserved name ${_picocolors.default.bold(value)} cannot be used`;
}
if (value.toLocaleLowerCase() !== value) {
return "Name must be lowercase";
}
if (/^\d/.test(value)) {
return "Name cannot start with a number";
}
if (/[`~!@#$%^&*()_=+\[\]{}|;:\'",<.>\/?\\]/.test(value)) {
return "No special characters allowed in name";
}
if (value.indexOf(' ') !== -1) {
return "No spaces allowed in name";
}
if (value.indexOf('-') === -1 || value.replace('--', '') !== value || value[0] === '-' || value[value.length - 1] === '-') {
return "Name must include at least one `-` and must not start or end name.";
}
// Check for any other syntax errors
if (!/^[a-z][a-z0-9.\-]*\-[a-z0-9.\-]*$/.test(value)) {
return `Name must follow the syntax ${_picocolors.default.bold("my-component")}`;
}
// assumes auto was selected in CLI
let joint = process.cwd();
if (commandRun.options.path) {
joint = commandRun.options.path;
}
if (fs.existsSync(path.join(joint, value))) {
return `${path.join(joint, value)} exists, rename this project`;
}
}
});
} else {
let value = commandRun.options.name;
if (!value) {
console.error(_picocolors.default.red("Name is required (tab writes default)"));
process.exit(1);
}
if (reservedNames.includes(value)) {
console.error(_picocolors.default.red(`Reserved name ${_picocolors.default.bold(value)} cannot be used`));
process.exit(1);
}
if (value.toLocaleLowerCase() !== value) {
console.error(_picocolors.default.red("Name must be lowercase"));
process.exit(1);
}
if (/^\d/.test(value)) {
console.error(_picocolors.default.red("Name cannot start with a number"));
process.exit(1);
}
if (/[`~!@#$%^&*()_=+\[\]{}|;:\'",<.>\/?\\]/.test(value)) {
console.error(_picocolors.default.red("No special characters allowed in name"));
process.exit(1);
}
if (value.indexOf(' ') !== -1) {
console.error(_picocolors.default.red("No spaces allowed in name"));
process.exit(1);
}
if (value.indexOf('-') === -1 || value.replace('--', '') !== value || value[0] === '-' || value[value.length - 1] === '-') {
console.error(_picocolors.default.red("Name must include at least one `-` and must not start or end name."));
process.exit(1);
}
// Check for any other syntax errors
if (!/^[a-z][a-z0-9.\-]*\-[a-z0-9.\-]*$/.test(value)) {
console.error(_picocolors.default.red(`Name must follow the syntax ${_picocolors.default.bold("my-component")}`));
process.exit(1);
}
// assumes auto was selected in CLI
let joint = process.cwd();
if (commandRun.options.path) {
joint = commandRun.options.path;
}
if (fs.existsSync(path.join(joint, value))) {
console.error(_picocolors.default.red(`${path.join(joint, value)} exists, rename this project`));
process.exit(1);
}
}
const project = {
name: commandRun.options.name,
path: activeHaxsite.directory,
className: (0, _utils.dashToCamel)(commandRun.options.name),
year: new Date().getFullYear()
};
if (!project.author) {
try {
let value = await (0, _utils.exec)(`git config user.name`);
project.author = value.stdout.trim();
} catch (e) {
(0, _logging.log)(`
git user name not configured. Run the following to do this:\n
git config --global user.name "namehere"\n
git config --global user.email "email@here`, 'debug');
}
}
const filePath = `${project.path}/custom/src/${project.name}.js`;
await fs.copyFileSync(`${process.mainModule.path}/templates/generic/sitecomponent.js`, filePath);
const ejsString = ejs.fileLoader(filePath, 'utf8');
let content = ejs.render(ejsString, project);
// file written successfully
fs.writeFileSync(filePath, content);
if (!commandRun.options.npmClient) {
commandRun.options.npmClient = 'npm';
}
if (fs.existsSync(`${project.path}/custom/custom-elements.json`)) {
await (0, _utils.exec)(`cd custom && ${commandRun.options.npmClient} run analyze`);
}
p.note(`🧙 Add to another web component (.js): ${_picocolors.default.underline(_picocolors.default.bold(_picocolors.default.yellow(_picocolors.default.bgBlack(`import ./${project.name}.js`))))}`);
// at least a second to see the message print at all
await (0, _promises.setTimeout)(1000);
} catch (e) {
(0, _logging.log)(e.stderr);
// Original ejs.render error checking
console.error(_picocolors.default.red(process.cwd()));
console.error(_picocolors.default.red(e));
}
break;
case "site:surge":
try {
// attempt to install; implies they asked to publish with surge but
// system test did not see it globally
if (!sysSurge) {
let s = p.spinner();
s.start((0, _statements.merlinSays)('Installing Surge.sh globally so we can publish'));
let execOutput = await (0, _utils.exec)(`npm install --global surge`);
s.stop((0, _statements.merlinSays)('surge.sh installed globally'));
(0, _logging.log)(execOutput.stdout.trim());
sysSurge = true;
}
let execOutput;
if (commandRun.options.domain && commandRun.options.y) {
let s = p.spinner();
s.start((0, _statements.merlinSays)('Sending site to Surge.sh ..'));
execOutput = await (0, _utils.exec)(`cd ${activeHaxsite.directory} && surge . ${commandRun.options.domain}`);
(0, _logging.log)(execOutput.stdout.trim());
s.stop((0, _statements.merlinSays)(`Site published: https://${commandRun.options.domain}`));
} else {
let surgeArgs = ['.'];
// could get here bc of being interactive, yet passed in a domain...
if (commandRun.options.domain) {
surgeArgs.push(commandRun.options.domain);
}
execOutput = await (0, _utils.interactiveExec)('surge', surgeArgs, {
cwd: activeHaxsite.directory
});
(0, _logging.log)((0, _statements.merlinSays)(`Site published: https://${commandRun.options.domain}`));
}
} catch (e) {
console.log("?");
(0, _logging.log)(e.stderr);
}
break;
case "site:file-list":
let res = new Res();
await hax.RoutesMap.get.listFiles({
query: activeHaxsite.name,
filename: commandRun.options.filename
}, res);
if (commandRun.options.format === 'yaml') {
(0, _logging.log)((0, _jsYaml.dump)(res.data));
} else {
(0, _logging.log)(res.data);
}
break;
case "site:html":
case "site:md":
case "site:schema":
let siteContent = '';
activeHaxsite = await hax.systemStructureContext();
let items = [];
if (commandRun.options.itemId != null) {
items = activeHaxsite.manifest.findBranch(commandRun.options.itemId);
} else {
items = activeHaxsite.manifest.orderTree(activeHaxsite.manifest.items);
}
if (operation.action === 'site:schema') {
let els = [];
for (var i in items) {
let page = activeHaxsite.loadNode(items[i].id);
let html = await activeHaxsite.getPageContent(page);
let dom = (0, _nodeHtmlParser.parse)(`<div id="fullpage">${html}</div>`);
els.push({
tag: "h1",
properties: {
"data-jos-item-id": items[i].id
},
content: `${items[i].title}`
});
for (var j in dom.querySelector('#fullpage').childNodes) {
let node = dom.querySelector('#fullpage').childNodes[j];
if (node && node.getAttribute) {
els.push(await nodeToHaxElement(node, null));
}
}
}
// simple redirecting to file
if (commandRun.options.toFile) {
if (commandRun.options.format === 'yaml') {
fs.writeFileSync(commandRun.options.toFile, (0, _jsYaml.dump)(els));
} else {
fs.writeFileSync(commandRun.options.toFile, JSON.stringify(els, null, 2));
}
} else {
if (commandRun.options.format === 'yaml') {
(0, _logging.log)((0, _jsYaml.dump)(els));
} else {
(0, _logging.log)(els);
}
}
} else {
for (var i in items) {
let page = activeHaxsite.loadNode(items[i].id);
siteContent += `<h1>${items[i].title}</h1>\n\r`;
siteContent += `<div data-jos-item-id="${items[i].id}">\n\r${await activeHaxsite.getPageContent(page)}\n\r</div>\n\r`;
}
if (operation.action === 'site:md') {
let resp = await openApiBroker('@core', 'htmlToMd', {
html: siteContent
});
if (commandRun.options.toFile) {
fs.writeFileSync(commandRun.options.toFile, resp.res.data.data);
if (!commandRun.options.quiet) {
(0, _logging.log)(`${commandRun.options.toFile} written`);
}
} else {
(0, _logging.log)(resp.res.data.data);
}
} else {
if (commandRun.options.toFile) {
fs.writeFileSync(commandRun.options.toFile, siteContent);
if (!commandRun.options.quiet) {
(0, _logging.log)(`${commandRun.options.toFile} written`);
}
} else {
(0, _logging.log)(siteContent);
}
}
}
break;
// @todo need to make these work..
case "recipe:read":
// just print the recipe out
if (fs.existsSync(path.join(process.cwd(), `${siteRecipeFile}`))) {
let recContents = await fs.readFileSync(path.join(process.cwd(), `${siteRecipeFile}`), 'utf8');
console.log(recContents);
}
break;
case "recipe:play":
// step through and run each recipe once fed a file location
// this allows for storing commands from a site and then replaying them with ease
if (!commandRun.options.recipe) {
commandRun.options.recipe = await p.text({
message: `Select recipe:`,
defaultValue: process.cwd(),
initialValue: process.cwd(),
validate: val => {
if (!val.endsWith('.recipe')) {
return 'HAX Recipe files must end in .recipe';
}
}
});
}
if (fs.existsSync(commandRun.options.recipe)) {
let recContents = await fs.readFileSync(commandRun.options.recipe, 'utf8');
// split into commands
let commandList = recContents.replaceAll('cli: ', '').split("\n");
let rootDir = '';
// confirm each command or allow --y so that it auto applies
for (var i in commandList) {
// verify every command starts this way for safety
if (commandList[i].startsWith('hax site')) {
let confirmation;
if (commandRun.options.y) {
confirmation = true;
} else {
confirmation = await p.confirm({
message: `Do you want to run ${commandList[i]}? (This cannot be undone)`,
initialValue: true
});
}
// confirmed; let's run!
if (confirmation) {
let commandMatch = siteActions().filter(action => action.value === commandList[i].split(' ')[2]);
// if we found a command that means it is a valid command to run against the site
if (commandMatch.length > 0) {
await (0, _utils.exec)(`${commandList[i]} --y --no-i --auto --quiet${rootDir}`);
}
// 1st command won't match as the argument creates a new site
// but ensure we don't have a site context prior to running this
// or we'll get a site in a site with the same name which is not
// the desired result
else if (!(await hax.systemStructureContext())) {
await (0, _utils.exec)(`${commandList[i]} --y --no-i --auto --quiet --no-extras`);
// site will have been created, obtain the site name and set root so
// the other commands get piped into it correctly
rootDir = ` --root ${commandList[i].split(' ')[2]}`;
} else {
(0, _logging.log)('Did not run because we already have a site', 'warn');
}
}
}
}
}
break;
case "issue:general":
// open the issues
p.intro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Submit issue / suggestion on Github `))}`);
p.intro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Opening in browser `))}`);
await (0, _open.default)("https://github.com/haxtheweb/issues/issues/new");
p.outro(`${_picocolors.default.bgBlue(_picocolors.default.white(` https://github.com/haxtheweb/issues/issues/new `))}`);
break;
case "issue:theme":
// open the issues
p.intro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Submit custom theme on Github `))}`);
p.intro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Opening in browser `))}`);
let allContents = '';
fs.readdir(`${activeHaxsite.directory}/custom/src`, (err, files) => {
if (err) {
console.error('Error reading directory:', err);
return;
}
files.forEach((file, index) => {
const filePath = path.join(`${activeHaxsite.directory}/custom/src`, file);
if (fs.lstatSync(filePath).isFile()) {
const content = fs.readFileSync(filePath, 'utf-8');
// append file name as a JS comment
allContents += `\n// FILENAME: ${file}\n` + content + '\n'; // Add newline between files if desired
}
if (index === files.length - 1) {
console.log('Combined contents of all files:\n', allContents);
}
});
});
console.log(allContents);
await (0, _open.default)(`https://github.com/haxtheweb/issues/issues/new?template=new-design.md`);
p.outro(`${_picocolors.default.bgBlue(_picocolors.default.white(` Copy the output of the console into the issue's js template area `))}`);
p.outro(`${_picocolors.default.bgBlue(_picocolors.default.white(` https://github.com/haxtheweb/issues/issues/new?template=new-design.md `))}`);
break;
case "quit":
// quit
process.exit(0);
break;
}
// y or noi need to act like it ran and finish instead of looping options
if (commandRun.options.y || !commandRun.options.i || !actionAssigned) {
process.exit(0);
}
operation.action = null;
}
if (!commandRun.options.quiet) {
(0, _statements.communityStatement)();
}
}
function siteNodeStatsOperations(search = null) {
let obj = [{
value: 'details',
label: "Details"
}, {
value: 'html',
label: "Page as HTML source"
}, {
value: 'schema',
label: "Page as HAXElementSchema"
}, {
value: 'md',
label: "Page as Markdown"
}];
if (search) {
for (const op of obj) {
if (op.value === search) {
return true;
}
}
return false;
}
return obj;
}
function siteNodeOperations(search = null) {
let obj = [{
value: 'title',
label: "Title"
}, {
value: 'content',
label: "Page content"
}, {
value: 'slug',
label: "Path (slug)"
}, {
value: 'published',
label: "Publishing status"
}, {
value: 'tags',
label: "Tags"
}, {
value: 'parent',
label: "Parent"
}, {
value: 'order',
label: "Order"
}, {
value: 'theme',
label: "Theme"
}, {
value: 'hideInMenu',
label: "Hide in menu"
}];
if (search) {
for (const op of obj) {
if (op.value === search) {
return true;
}
}
return false;
}
return obj;
}
// broker a call to the open-api repo which is an express based wrapper for vercel (originally)
// this ensures the calls are identical and yet are converted to something the CLI can leverage
async function openApiBroker(scope, call, body) {
let mfItem = _microFrontendRegistry.MicroFrontendRegistry.get(`${scope}/${call}`);
// ensure we have a MFR record to do the connection
// fun thing is this is local file access directly via import()
if (mfItem) {
// dynamic import... this might upset some stuff later bc it's not a direct reference
// but it's working locally at least.
const handler = await (specifier => new Promise(r => r(specifier)).then(s => _interopRequireWildcard(require(s))))(`${mfItem.endpoint.replace('/api/', '/dist/')}.js`);
let res = new Res();
let req = {
body: JSON.stringify(body),
method: "post"
};
// js pass by ref for the win; these will both update bc of how we structured the calls
await handler.default(req, res);
// they'll need unpacked but that's a small price!
return {
req: req,
res: res
};
}
}
// process site creation
async function siteProcess(commandRun, project, port = '3000') {
// auto select operations to perform if requested
var s = p.spinner();
// if we have no extras, or they are empty then set for launch
if (!project.extras) {
project.extras = [];
if (commandRun.options.i) {
project.extras = ['launch'];
}
}
let siteRequest = {
"site": {
"name": project.name,
"description": "own course",
"theme": commandRun.options.theme ? commandRun.options.theme : project.theme ? project.theme : "clean-one"
},
"build": {
"type": "own",
"structure": "course",
"items": null,
"files": null
},
"theme": {
"color": "green",
"icon": "av:library-add"
}
};
// allow for importSite option
if (commandRun.options.importSite) {
if (!commandRun.options.importStructure) {
// assume hax to hax if it's not defined
commandRun.options.importStructure = 'haxcmsToSite';
}
// verify this is a valid way to do an import
if (commandRun.options.importStructure && _microFrontendRegistry.MicroFrontendRegistry.get(`@haxcms/${commandRun.options.importStructure}`)) {
let resp = await openApiBroker('@haxcms', commandRun.options.importStructure, {
repoUrl: commandRun.options.importSite
});
if (resp.res.data && resp.res.data.data && resp.res.data.data.items) {
siteRequest.build.structure = 'import';
siteRequest.build.items = resp.res.data.data.items;
}
if (resp.res.data && resp.res.data.data && resp.res.data.data.files) {
siteRequest.build.files = resp.res.data.data.files;
}
}
// hidden import methodologies
else if (commandRun.options.importStructure) {
if (commandRun.options.importStructure === 'drupal7-book-print-html') {
let siteContent = await fetch(commandRun.options.importSite).then(d => d.ok ? d.text() : '');
if (siteContent) {
// @todo refactor to support 9 levels of hierarchy as this is technically what Drupal supports
let dom = (0, _nodeHtmlParser.parse)(siteContent);
// pull all of level 1 of hierarchy
let depth;
let order = 0;
let parent = null;
let items = [];
for (let branch1 of dom.querySelectorAll('.section-2')) {
parent = null;
depth = 0;
let itemID = branch1.getAttribute('id');
let item = {
id: itemID,
order: order,
indent: depth,
title: branch1.querySelector('h1').innerText,
slug: itemID.replace('-', '/'),
contents: branch1.querySelector(`.field.field-name-body .field-item`).innerHTML,
parent: parent
};
items.push(item);
order++;
depth = 1;
let parent2 = itemID;
let order2 = 0;
for (let branch2 of branch1.querySelectorAll('.section-3')) {
itemID = branch2.getAttribute('id');
let item = {
id: itemID,
order: order2,
indent: depth,
title: branch2.querySelector('h1').innerText,
slug: itemID.replace('-', '/'),
contents: branch2.querySelector(`.field.field-name-body .field-item`).innerHTML,
parent: parent2
};
items.push(item);
order2++;
depth = 2;
let parent3 = itemID;
let order3 = 0;
for (let branch3 of branch2.querySelectorAll('.section-4')) {
itemID = branch3.getAttribute('id');
let item = {
id: itemID,
order: order3,
indent: depth,
title: branch3.querySelector('h1').innerText,
slug: itemID.replace('-', '/'),
contents: branch3.querySelector(`.field.field-name-body .field-item`).innerHTML,
parent: parent3
};
items.push(item);
order3++;
}
}
// obtain all images on the system to bring along with additional spider request
let location = new URL(commandRun.options.importSite).origin;
var files = {};
for (let image of dom.querySelectorAll("img[src^='/']")) {
if (!image.getAttribute('src').startsWith('//')) {
files[image.getAttribute('src')] = `${location}${image.getAttribute('src')}`;
}
}
siteRequest.build.files = files;
}
siteRequest.build.structure = 'import';
siteRequest.build.items = items;
}
}
}
}
HAXCMS.cliWritePath = `${project.path}`;
let res = new Res();
// unfortunately the twig exception is not blockable from output at this layer
await hax.RoutesMap.post.createSite({
body: siteRequest,
query: {
user_token: "fakeToken",
site_token: "fakeToken"
}
}, res);
// so we run it and then clear the screen
// this is a bit of a hack but it works to give the user the feedback that the site was
// created successfully, but only if not in quiet mode (default)
if (!commandRun.options.quiet) {
process.stdout.write('\x1Bc');
s.start((0, _statements.merlinSays)(`Creating new site: ${project.name}`));
await (0, _promises.setTimeout)(1000);
}
// path different for this one as it's on the fly produced
const recipeFileName = path.join(project.path, '/', project.name, `${siteRecipeFile}`);
const recipeLogTransport = new winston.transports.File({
filename: recipeFileName
});
const recipe = winston.createLogger({
levels: logLevels,
level: siteLoggingName,
transports: [recipeLogTransport],
format: winston.format.simple()
});
// matching the common object elsewhere tho different reference in this command since it creates from nothing
// capture this if use input on the fly
if (!commandRun.arguments.action) {
commandRun.arguments.action = project.name;
}
commandRun.options.theme = project.theme;
recipe.log(siteLoggingName, (0, _logging.commandString)(commandRun));
if (commandRun.options.v) {
if (commandRun.options.format === 'yaml') {
(0, _logging.log)((0, _jsYaml.dump)(res.data), 'silly');
} else {
(0, _logging.log)(res.data, 'silly');
}
}
if (!commandRun.options.quiet) {
s.stop((0, _statements.merlinSays)(`${project.name} created successfully!`));
await (0, _promises.setTimeout)(500);
}
// Write theme template to site/custom
if (commandRun.options.theme === 'custom-theme' && commandRun.options.customThemeName && commandRun.options.customThemeTemplate || project.customThemeName && project.customThemeTemplate) {
s.start((0, _statements.merlinSays)(`Creating new theme: ${commandRun.options.customThemeName ? commandRun.options.customThemeName : project.customThemeName}`));
await customSiteTheme(commandRun, project);
s.stop((0, _statements.merlinSays)(`${commandRun.options.customThemeName ? commandRun.options.customThemeName : project.customThemeName} theme created!`));
}
if (project.gitRepo && !commandRun.options.isMonorepo) {
try {
await (0, _utils.exec)(`cd ${project.path}/${project.name} && git init && git add -A && git commit -m "first commit" && git branch -M main${project.gitRepo ? ` && git remote add origin ${project.gitRepo}` : ''}`);
} catch (e) {}
}
// ensure dot files is there because it doesn't copy for some reason for sites :\
if (!fs.existsSync(`${project.path}/${project.name}/.gitignore`)) {
await fs.copyFileSync(`${process.mainModule.path}/templates/sitedotfiles/_gitignore`, `${project.path}/${project.name}/.gitignore`);
}
if (!fs.existsSync(`${project.path}/${project.name}/._npmignore`)) {
await fs.copyFileSync(`${process.mainModule.path}/templates/sitedotfiles/_npmignore`, `${project.path}/${project.name}/.npmignore`);
}
if (!fs.existsSync(`${project.path}/${project.name}/._surgeignore`)) {
await fs.copyFileSync(`${process.mainModule.path}/templates/sitedotfiles/_surgeignore`, `${project.path}/${project.name}/.surgeignore`);
}
// options for install, git and other extras
// can't launch if we didn't install first so launch implies installation
if (project.extras && project.extras.includes && project.extras.includes('launch')) {
let optionPath = `${project.path}/${project.name}`;
let command = `npx @haxtheweb/haxcms-nodejs`;
if (!commandRun.options.quiet) {
p.note(`${(0, _statements.merlinSays)(`I have summoned a sub-process daemon 👹`)}
🚀 Running your ${_picocolors.default.bold(project.type)} ${_picocolors.default.bold(project.name)}:
${_picocolors.default.underline(_picocolors.default.cyan(`http://localhost:${port}`))}
🏠 Launched: ${_picocolors.default.underline(_picocolors.default.bold(_picocolors.default.yellow(_picocolors.default.bgBlack(`${optionPath}`))))}
💻 Folder: ${_picocolors.default.bold(_picocolors.default.yellow(_picocolors.default.bgBlack(`cd ${optionPath}`)))}
📂 Open folder: ${_picocolors.default.bold(_picocolors.default.yellow(_picocolors.default.bgBlack(`open ${optionPath}`)))}
📘 VS Code Project: ${_picocolors.default.bold(_picocolors.default.yellow(_picocolors.default.bgBlack(`code ${optionPath}`)))}
🚧 Launch later: ${_picocolors.default.bold(_picocolors.default.yellow(_picocolors.default.bgBlack(`${command}`)))}
⌨️ To resume 🧙 Merlin press: ${_picocolors.default.bold(_picocolors.default.black(_picocolors.default.bgRed(` CTRL + C or CTRL + BREAK `)))}
`);
}
// at least a second to see the message print at all
await (0, _promises.setTimeout)(1000);
try {
await (0, _utils.exec)(`cd ${optionPath} && ${command}`);
} catch (e) {
// don't log bc output is weird
}
} else if (!commandRun.options.quiet) {
let nextSteps = `cd ${project.path}/${project.name} && hax start`;
p.note(`${project.name} is ready to go. Run the following to start working with it:`);
p.outro(nextSteps);
}
}
async function siteItemsOptionsList(activeHaxsite, skipId = null) {
let items = activeHaxsite.manifest.orderTree(activeHaxsite.manifest.items);
let optionItems = [];
for (var i in items) {
// ensure we remove self if operation is about page in question like parent selector
if (items[i].id !== skipId) {
optionItems.push({
value: items[i].id,
label: ` ${'-'.repeat(parseInt(items[i].indent))}${items[i].title}`
});
}
}
return optionItems;
}
async function siteThemeList(coreOnly = true, directory = null) {
let items = [];
if (coreOnly) {
items = [{
value: 'clean-one',
label: 'Clean One'
}, {
value: 'clean-two',
label: 'Clean Two'
}, {
value: 'clean-portfolio-theme',
label: 'Clean Portfolio'
}, {
value: 'journey-theme',
label: 'Journey'
}, {
value: 'polaris-flex-theme',
label: 'Flex'
}, {
value: 'polaris-flex-sidebar',
label: 'Flex Sidebar'
}, {
value: 'custom-theme',
label: 'Create Custom Theme'
}];
if (fs.existsSync(`${directory}/custom/custom-elements.json`)) {
let customThemeArray = JSON.parse(fs.readFileSync(`${directory}/custom/custom-elements.json`, 'utf8')).modules;
for (var i in customThemeArray) {
if (customThemeArray[i].declarations[0].superclass.name === "PolarisFlexTheme" || customThemeArray[i].declarations[0].superclass.name === "HAXCMSLitElementTheme") {
items.push({
value: customThemeArray[i].declarations[0].tagName,
label: customThemeArray[i].declarations[0].name
});
}
}
}
} else {
let themes = await HAXCMS.getThemes();
for (var i in themes) {
items.push({
value: i,
label: themes[i].name
});
}
}
return items;
}
async function customSiteTheme(commandRun, project) {
// pass theme name for twig templates
project.customThemeName = commandRun.options.customThemeName ? commandRun.options.customThemeName : project.customThemeName;
// validate start and end tags for theme name
if (/^custom/.test(project.customThemeName) && !/^custom-/.test(project.customThemeName)) {
project.customThemeName = project.customThemeName.replace(/^custom/, "custom-");
} else if (!/^custom-/.test(project.customThemeName)) {
project.customThemeName = `custom-${project.customThemeName}`;
}
if (/theme$/.test(project.customThemeName) && !/-theme$/.test(project.customThemeName)) {
project.customThemeName = project.customThemeName.replace(/theme$/, "-theme");
} else if (!/-theme$/.test(project.customThemeName)) {
project.customThemeName = `${project.customThemeName}-theme`;
}
// set camel case class name
project.className = (0, _utils.dashToCamel)(project.customThemeName);
// path to hax site
var sitePath;
if (!commandRun.options.directory) {
sitePath = `${commandRun.options.path ? commandRun.options.path : project.path}/${commandRun.options.name ? commandRun.options.name : project.name}`;
} else {
// existing sites
sitePath = commandRun.options.directory;
}
// path to new theme file
const filePath = `${sitePath}/custom/src/${project.customThemeName}.js`;
if (!project.year) {
project.year = new Date().getFullYear();
}
if (!project.author) {
try {
let value = await (0, _utils.exec)(`git config user.name`);
project.author = value.stdout.trim();
} catch (e) {
(0, _logging.log)(`
git user name not configured. Run the following to do this:\n
git config --global user.name "namehere"\n
git config --global user.email "email@here`, 'debug');
}
}
// theme template to use
const themeTemplate = commandRun.options.customThemeTemplate ? commandRun.options.customThemeTemplate : project.customThemeTemplate;
if (themeTemplate === "polaris-flex") {
await fs.copyFileSync(`${process.mainModule.path}/templates/sitetheme/flex-theme.js`, `${sitePath}/custom/src/flex-theme.js`);
await fs.renameSync(`${sitePath}/custom/src/flex-theme.js`, filePath);
} else if (themeTemplate === "polaris-sidebar") {
await fs.copyFileSync(`${process.mainModule.path}/templates/sitetheme/sidebar-theme.js`, `${sitePath}/custom/src/sidebar-theme.js`);
await fs.renameSync(`${sitePath}/custom/src/sidebar-theme.js`, filePath);
} else {
// vanilla theme is default
await fs.copyFileSync(`${process.mainModule.path}/templates/sitetheme/base-theme.js`, `${sitePath}/custom/src/base-theme.js`);
await fs.renameSync(`${sitePath}/custom/src/base-theme.js`, filePath);
}
try {
// ensure we don't try to pattern rewrite image files
if (!filePath.endsWith('.jpg') && !filePath.endsWith('.png')) {
const ejsString = ejs.fileLoader(filePath, 'utf8');
let content = ejs.render(ejsString, project);
// file written successfully
fs.writeFileSync(filePath, content);
}
} catch (err) {
console.error(filePath);
console.error(err);
}
// import theme to custom.js
await fs.appendFileSync(`${sitePath}/custom/src/custom.js`, `\n import "./${project.customThemeName}.js";`);
var activeHaxsite = await hax.systemStructureContext(sitePath);
// add theme to site.json
let themeObj = {
element: project.customThemeName,
path: "./custom/build/custom.es6.js",
name: project.className
};
activeHaxsite.manifest.metadata.theme = themeObj;
activeHaxsite.manifest.save(false);
// install and build theme dependencies
await (0, _utils.exec)(`cd ${sitePath}/custom/ && ${commandRun.options.npmClient} install && ${commandRun.options.npmClient} run build && ${commandRun.options.npmClient} run analyze && cd ${sitePath}`);
}
// @fork of the hax core util for this so that we avoid api difference between real dom and parse nodejs dom
async function nodeToHaxElement(node, eventName = "insert-element") {
if (!node) {
return null;
}
// build out the properties to send along
var props = {};
// support basic styles
if (typeof node.getAttribute("style") !== typeof undefined) {
props.style = node.getAttribute("style");
}
// don't set a null style
if (props.style === null || props.style === "null") {
delete props.style;
}
// test if a class exists, not everything scopes
if (typeof node.getAttribute('class') !== typeof undefined) {
props.class = node.getAttribute('class').replace("hax-active", "");
}
// test if a id exists as its a special case in attributes... of course
if (typeof node.getAttribute('id') !== typeof undefined) {
props.id = node.getAttribute("id");
}
let tmpProps;
// weak fallback
if (typeof tmpProps === typeof undefined) {
tmpProps = node.__data;
}
// complex elements need complex support
if (typeof tmpProps !== typeof undefined) {
// run through attributes, though non-reflected props won't be here
// run through props, we always defer to property values
for (var property in tmpProps) {
// make sure we only set things that have a value
if (property != "class" && property != "style" && tmpProps.hasOwnProperty(property) && typeof node[property] !== undefined && node[property] != null && node[property] != "") {
props[property] = node[property];
}
// special support for false boolean
else if (node[property] === false) {
props[property] = false;
} else if (node[property] === true) {
props[property] = true;
} else if (node[property] === 0) {
props[property] = 0;
} else {
// unknown prop setting / ignored
//console.warn(node[property], property);
}
}
for (var attribute in node._attrs) {
// make sure we only set things that have a value
if (typeof node._attrs[attribute] !== typeof undefined && attribute != "class" && attribute != "style" && attribute != "id" && typeof node._attrs[attribute] !== undefined && node._attrs[attribute] != null && node._attrs[attribute] != "") {
props[attribute] = node._attrs[attribute];
} else if (node._attrs[attribute] == "0") {
props[attribute] = node._attrs[attribute];
} else {
// note: debug here if experiencing attributes that won't bind
}
}
} else {
// much easier case, usually just in primatives
for (var attribute in node._attrs) {
// make sure we only set things that have a value
if (typeof node._attrs[attribute] !== typeof undefined && attribute != "class" && attribute != "style" && attribute != "id" && typeof node._attrs[attribute] !== undefined && node._attrs[attribute] != null && node._attrs[attribute] != "") {
props[attribute] = node._attrs[attribute];
}
}
}
// support sandboxed environments which
// will hate iframe tags but love webview
let tag = node.tagName.toLowerCase();
if (globalThis.HaxStore && globalThis.HaxStore.instance && globalThis.HaxStore.instance._isSandboxed && tag === "iframe") {
tag = "webview";
}
let slotContent = '';
// if hax store around, allow it to get slot content of the node
if (globalThis.HaxStore && globalThis.HaxStore.instance) {
slotContent = await globalThis.HaxStore.instance.getHAXSlot(node);
} else {
// if HAX isn't around, just return the innerHTML as a string for asignment to content
slotContent = node.innerHTML;
}
// support fallback on inner text if there were no nodes
if (slotContent == "") {
slotContent = node.innerText;
}
let element = {
tag: tag,
properties: props,
content: slotContent
};
if (eventName !== null) {
element.eventName = eventName;
}
return element;
}