beta/code-blocks (#1)
Reviewed-on: #1 Co-authored-by: obvTiger <obvtiger@epilogue.team> Co-committed-by: obvTiger <obvtiger@epilogue.team>
This commit is contained in:
parent
362b7aa15e
commit
d125640fe7
26 changed files with 1816 additions and 102 deletions
|
@ -129,6 +129,41 @@ class ASTBuilder {
|
|||
);
|
||||
}
|
||||
|
||||
if (token.type === "client" || token.type === "server") {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[ASTBuilder] Processing ${token.type} block at line ${token.line}, column ${token.column}`
|
||||
);
|
||||
}
|
||||
|
||||
const node = {
|
||||
type: token.type,
|
||||
script: token.value,
|
||||
line: token.line,
|
||||
column: token.column,
|
||||
};
|
||||
|
||||
if (token.type === "server" && token.params) {
|
||||
node.params = token.params;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[ASTBuilder] Server block parameters: ${node.params.join(", ")}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`[ASTBuilder] Created node for ${token.type} block`);
|
||||
console.log(
|
||||
"[ASTBuilder] Script content (first 50 chars):",
|
||||
node.script.substring(0, 50) + (node.script.length > 50 ? "..." : "")
|
||||
);
|
||||
}
|
||||
|
||||
current++;
|
||||
return node;
|
||||
}
|
||||
|
||||
if (token.type === "identifier") {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
|
|
|
@ -46,6 +46,20 @@ class BlueprintBuilder {
|
|||
if (result.success) {
|
||||
this.fileHandler.writeCompiledFiles(outputDir, baseName, result.html, result.css);
|
||||
|
||||
if (result.hasServerCode && result.serverCode) {
|
||||
const serverDir = path.join(outputDir, 'server');
|
||||
if (!fs.existsSync(serverDir)) {
|
||||
fs.mkdirSync(serverDir, { recursive: true });
|
||||
}
|
||||
|
||||
const serverFilePath = path.join(serverDir, `${baseName}-server.js`);
|
||||
fs.writeFileSync(serverFilePath, result.serverCode, 'utf8');
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`[DEBUG] Server code written to ${serverFilePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("[DEBUG] Build completed successfully");
|
||||
}
|
||||
|
@ -54,6 +68,7 @@ class BlueprintBuilder {
|
|||
return {
|
||||
success: result.success,
|
||||
errors: result.errors,
|
||||
hasServerCode: result.hasServerCode
|
||||
};
|
||||
} catch (error) {
|
||||
if (this.options.debug) {
|
||||
|
@ -61,6 +76,7 @@ class BlueprintBuilder {
|
|||
}
|
||||
return {
|
||||
success: false,
|
||||
hasServerCode: false,
|
||||
errors: [
|
||||
{
|
||||
message: error.message,
|
||||
|
|
|
@ -51,18 +51,25 @@ class BlueprintCompiler {
|
|||
|
||||
const html = this.htmlGenerator.generateHTML(ast);
|
||||
const css = this.cssGenerator.generateCSS();
|
||||
const hasServerCode = this.htmlGenerator.hasServerCode();
|
||||
const serverCode = hasServerCode ? this.htmlGenerator.generateServerCode() : '';
|
||||
|
||||
const headContent = this.metadataManager.generateHeadContent(baseName);
|
||||
const finalHtml = this.htmlGenerator.generateFinalHtml(headContent, html);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("[DEBUG] Compilation completed successfully");
|
||||
if (hasServerCode) {
|
||||
console.log("[DEBUG] Server code generated");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
html: finalHtml,
|
||||
css: css,
|
||||
hasServerCode: hasServerCode,
|
||||
serverCode: serverCode,
|
||||
errors: [],
|
||||
};
|
||||
} catch (error) {
|
||||
|
@ -73,6 +80,8 @@ class BlueprintCompiler {
|
|||
success: false,
|
||||
html: null,
|
||||
css: null,
|
||||
hasServerCode: false,
|
||||
serverCode: null,
|
||||
errors: [
|
||||
{
|
||||
message: error.message,
|
||||
|
|
|
@ -6,6 +6,8 @@ const StandardElementGenerator = require("./generators/StandardElementGenerator"
|
|||
const RootNodeGenerator = require("./generators/RootNodeGenerator");
|
||||
const InputElementGenerator = require("./generators/InputElementGenerator");
|
||||
const MediaElementGenerator = require("./generators/MediaElementGenerator");
|
||||
const JavaScriptGenerator = require("./generators/JavaScriptGenerator");
|
||||
const ServerCodeGenerator = require("./generators/ServerCodeGenerator");
|
||||
const HTMLTemplate = require("./templates/HTMLTemplate");
|
||||
const StringUtils = require("./utils/StringUtils");
|
||||
|
||||
|
@ -21,6 +23,9 @@ class HTMLGenerator {
|
|||
this.options = options;
|
||||
this.cssGenerator = cssGenerator;
|
||||
this.htmlTemplate = new HTMLTemplate(options);
|
||||
this.serverGenerator = new ServerCodeGenerator(options);
|
||||
this.jsGenerator = new JavaScriptGenerator(options, this.serverGenerator);
|
||||
this.currentElement = null;
|
||||
|
||||
|
||||
this.generators = [
|
||||
|
@ -86,6 +91,10 @@ class HTMLGenerator {
|
|||
console.log("[HTMLGenerator] Node details:", StringUtils.safeStringify(node));
|
||||
}
|
||||
|
||||
// Handle client and server blocks
|
||||
if (node.type === "client" || node.type === "server") {
|
||||
return this.handleScriptBlock(node);
|
||||
}
|
||||
|
||||
if (node.type === "element" && node.tag === "page") {
|
||||
if (this.options.debug) {
|
||||
|
@ -94,20 +103,120 @@ class HTMLGenerator {
|
|||
return "";
|
||||
}
|
||||
|
||||
const prevElement = this.currentElement;
|
||||
this.currentElement = node;
|
||||
|
||||
// Check if this element has an explicit ID in its props
|
||||
if (node.type === "element") {
|
||||
const idProp = node.props.find(p => typeof p === "string" && p.startsWith("id:"));
|
||||
if (idProp) {
|
||||
const idValue = idProp.substring(idProp.indexOf(":") + 1).trim().replace(/^"|"$/g, "");
|
||||
node.elementId = idValue;
|
||||
|
||||
// Register this element as reactive
|
||||
this.jsGenerator.registerReactiveElement(idValue);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`[HTMLGenerator] Found explicit ID: ${idValue}, registered as reactive`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = "";
|
||||
for (const generator of this.generators) {
|
||||
if (generator.canHandle(node)) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[HTMLGenerator] Using ${generator.constructor.name} for node`);
|
||||
}
|
||||
return generator.generate(node);
|
||||
|
||||
// If this is an element that might have event handlers,
|
||||
// add a unique ID to it for client scripts if it doesn't already have one
|
||||
if (node.type === "element" && node.children.some(child => child.type === "client")) {
|
||||
// Generate a unique ID for this element if it doesn't already have one
|
||||
if (!node.elementId) {
|
||||
node.elementId = this.jsGenerator.generateElementId();
|
||||
if (this.options.debug) {
|
||||
console.log(`[HTMLGenerator] Generated ID for element: ${node.elementId}`);
|
||||
}
|
||||
}
|
||||
|
||||
result = generator.generate(node);
|
||||
|
||||
// Process all client blocks inside this element
|
||||
node.children
|
||||
.filter(child => child.type === "client")
|
||||
.forEach(clientBlock => {
|
||||
this.handleScriptBlock(clientBlock, node.elementId);
|
||||
});
|
||||
} else {
|
||||
result = generator.generate(node);
|
||||
}
|
||||
|
||||
this.currentElement = prevElement;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`[HTMLGenerator] No generator found for node type: ${node.type}`);
|
||||
}
|
||||
|
||||
this.currentElement = prevElement;
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles client and server script blocks.
|
||||
* @param {Object} node - The script node to handle
|
||||
* @param {string} [elementId] - The ID of the parent element, if any
|
||||
* @returns {string} - Empty string as script blocks don't directly generate HTML
|
||||
*/
|
||||
handleScriptBlock(node, elementId = null) {
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[HTMLGenerator] Processing ${node.type} script block`);
|
||||
console.log(`[HTMLGenerator] Script content (first 50 chars): "${node.script.substring(0, 50)}..."`);
|
||||
if (elementId) {
|
||||
console.log(`[HTMLGenerator] Attaching to element: ${elementId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === "client") {
|
||||
if (!elementId && this.currentElement) {
|
||||
if (!this.currentElement.elementId) {
|
||||
this.currentElement.elementId = this.jsGenerator.generateElementId();
|
||||
}
|
||||
elementId = this.currentElement.elementId;
|
||||
}
|
||||
|
||||
if (elementId) {
|
||||
this.jsGenerator.addClientScript(elementId, node.script);
|
||||
} else {
|
||||
if (this.options.debug) {
|
||||
console.log(`[HTMLGenerator] Warning: Client script with no parent element`);
|
||||
}
|
||||
}
|
||||
} else if (node.type === "server") {
|
||||
if (!elementId && this.currentElement) {
|
||||
if (!this.currentElement.elementId) {
|
||||
this.currentElement.elementId = this.jsGenerator.generateElementId();
|
||||
}
|
||||
elementId = this.currentElement.elementId;
|
||||
}
|
||||
|
||||
if (elementId) {
|
||||
const params = node.params || [];
|
||||
if (this.options.debug && params.length > 0) {
|
||||
console.log(`[HTMLGenerator] Server block parameters: ${params.join(", ")}`);
|
||||
}
|
||||
|
||||
this.jsGenerator.addServerScript(elementId, node.script, params);
|
||||
} else {
|
||||
if (this.options.debug) {
|
||||
console.log(`[HTMLGenerator] Warning: Server script with no parent element`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -177,7 +286,28 @@ class HTMLGenerator {
|
|||
* @returns {string} - A complete HTML document containing the provided head and body content.
|
||||
*/
|
||||
generateFinalHtml(headContent, bodyContent) {
|
||||
return this.htmlTemplate.generateDocument(headContent, bodyContent);
|
||||
const clientScripts = this.jsGenerator.generateClientScripts();
|
||||
|
||||
return this.htmlTemplate.generateDocument(
|
||||
headContent,
|
||||
bodyContent + clientScripts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates server-side code for Express.js API routes.
|
||||
* @returns {string} - Express.js server code
|
||||
*/
|
||||
generateServerCode() {
|
||||
return this.serverGenerator.generateServerCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is any server code to generate.
|
||||
* @returns {boolean} - Whether there is server code
|
||||
*/
|
||||
hasServerCode() {
|
||||
return this.serverGenerator.hasServerCodeToGenerate();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
179
lib/StandardElementGenerator.js
Normal file
179
lib/StandardElementGenerator.js
Normal file
|
@ -0,0 +1,179 @@
|
|||
/**
|
||||
* Generates HTML for standard HTML elements.
|
||||
*/
|
||||
class StandardElementGenerator {
|
||||
/**
|
||||
* Creates a new StandardElementGenerator instance.
|
||||
* @param {Object} options - Generator options
|
||||
* @param {CSSGenerator} cssGenerator - CSS generator instance
|
||||
* @param {HTMLGenerator} htmlGenerator - HTML generator instance
|
||||
*/
|
||||
constructor(options = {}, cssGenerator, htmlGenerator) {
|
||||
this.options = options;
|
||||
this.cssGenerator = cssGenerator;
|
||||
this.htmlGenerator = htmlGenerator;
|
||||
this.parentGenerator = htmlGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this generator can handle a given node.
|
||||
* @param {Object} node - Node to check
|
||||
* @returns {boolean} - True if this generator can handle the node
|
||||
*/
|
||||
canHandle(node) {
|
||||
return node.type === "element";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML for a standard element.
|
||||
* @param {Object} node - Node to generate HTML for
|
||||
* @returns {string} - Generated HTML
|
||||
*/
|
||||
generate(node) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[StandardElementGenerator] Processing element node: ${node.tag || node.name}`);
|
||||
}
|
||||
|
||||
const mapping = ELEMENT_MAPPINGS ? ELEMENT_MAPPINGS[node.tag] : null;
|
||||
|
||||
const tagName = mapping ? mapping.tag : (node.name || node.tag || "div");
|
||||
|
||||
let className = "";
|
||||
if (this.cssGenerator) {
|
||||
className = this.cssGenerator.generateClassName(node.tag);
|
||||
const { cssProps, nestedRules } = this.cssGenerator.nodeToCSSProperties(node);
|
||||
this.cssGenerator.cssRules.set(`.${className}`, {
|
||||
cssProps,
|
||||
nestedRules,
|
||||
});
|
||||
}
|
||||
|
||||
const fullClassName = this.generateClassName(node);
|
||||
if (className && fullClassName) {
|
||||
className = `${className} ${fullClassName}`;
|
||||
} else if (fullClassName) {
|
||||
className = fullClassName;
|
||||
}
|
||||
|
||||
const attributes = this.generateAttributes(node, node.elementId, className);
|
||||
|
||||
let content = "";
|
||||
const children = node.children || [];
|
||||
|
||||
for (const child of children) {
|
||||
if (
|
||||
child.type === "client" ||
|
||||
child.type === "server" ||
|
||||
(child.type === "element" && (child.name === "script" || child.tag === "script"))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
child.parent = node;
|
||||
content += this.htmlGenerator.generateHTML(child);
|
||||
if (!this.options.minified) {
|
||||
content += "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return `<${tagName}${attributes}>${this.options.minified ? "" : "\n"}${content}${this.options.minified ? "" : ""}</${tagName}>${this.options.minified ? "" : "\n"}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a className string for the element.
|
||||
* @param {Object} node - The node to generate class for
|
||||
* @returns {string} - The generated class name
|
||||
*/
|
||||
generateClassName(node) {
|
||||
let classNames = [];
|
||||
|
||||
if (node.attributes && Array.isArray(node.attributes)) {
|
||||
const classAttr = node.attributes.find(attr => attr.name === "class");
|
||||
if (classAttr && classAttr.value) {
|
||||
classNames.push(classAttr.value);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.props && Array.isArray(node.props)) {
|
||||
for (const prop of node.props) {
|
||||
if (typeof prop === "string") {
|
||||
if (prop.startsWith("class:")) {
|
||||
classNames.push(prop.substring(prop.indexOf(":") + 1).trim().replace(/^"|"$/g, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return classNames.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an attributes string for the element.
|
||||
* @param {Object} node - The node to generate attributes for
|
||||
* @param {string} id - The element ID
|
||||
* @param {string} className - The element class name
|
||||
* @returns {string} - The generated attributes string
|
||||
*/
|
||||
generateAttributes(node, id, className) {
|
||||
let attributes = "";
|
||||
|
||||
if (id) {
|
||||
attributes += ` id="${id}"`;
|
||||
} else if (node.props && Array.isArray(node.props)) {
|
||||
const idProp = node.props.find(p => typeof p === "string" && p.startsWith("id:"));
|
||||
if (idProp) {
|
||||
const idValue = idProp.substring(idProp.indexOf(":") + 1).trim().replace(/^"|"$/g, "");
|
||||
attributes += ` id="${idValue}"`;
|
||||
node.elementId = idValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (className) {
|
||||
attributes += ` class="${className}"`;
|
||||
}
|
||||
|
||||
if (node.props && Array.isArray(node.props)) {
|
||||
const dataProps = node.props.filter(p => typeof p === "string" && p.startsWith("data-"));
|
||||
if (dataProps.length) {
|
||||
attributes += " " + dataProps.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
if (node.attributes && Array.isArray(node.attributes)) {
|
||||
for (const attr of node.attributes) {
|
||||
if (attr.name === "id" || attr.name === "class") continue;
|
||||
|
||||
if (attr.value === true) {
|
||||
attributes += ` ${attr.name}`;
|
||||
} else if (attr.value !== false && attr.value !== undefined && attr.value !== null) {
|
||||
attributes += ` ${attr.name}="${attr.value}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.props && Array.isArray(node.props)) {
|
||||
for (const prop of node.props) {
|
||||
if (typeof prop === "string") {
|
||||
if (prop.startsWith("id:") || prop.startsWith("class:") || prop.startsWith("data-")) continue;
|
||||
|
||||
const colonIndex = prop.indexOf(":");
|
||||
if (colonIndex !== -1) {
|
||||
const name = prop.substring(0, colonIndex).trim();
|
||||
const value = prop.substring(colonIndex + 1).trim().replace(/^"|"$/g, "");
|
||||
|
||||
if (!attributes.includes(` ${name}="`)) {
|
||||
attributes += ` ${name}="${value}"`;
|
||||
}
|
||||
} else {
|
||||
if (!attributes.includes(` ${prop}`)) {
|
||||
attributes += ` ${prop}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StandardElementGenerator;
|
|
@ -102,6 +102,142 @@ class TokenParser {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (char === "@") {
|
||||
const startPos = current;
|
||||
const startColumn = column;
|
||||
const startLine = line;
|
||||
current++;
|
||||
column++;
|
||||
|
||||
let blockType = "";
|
||||
char = input[current];
|
||||
|
||||
while (current < input.length && /[a-zA-Z]/.test(char)) {
|
||||
blockType += char;
|
||||
current++;
|
||||
column++;
|
||||
char = input[current];
|
||||
}
|
||||
|
||||
if (blockType === "client" || blockType === "server") {
|
||||
if (this.options.debug) {
|
||||
console.log(`[TokenParser] ${blockType} block found at line ${startLine}, column ${startColumn}`);
|
||||
}
|
||||
|
||||
while (current < input.length && /\s/.test(char)) {
|
||||
if (char === "\n") {
|
||||
line++;
|
||||
column = 1;
|
||||
} else {
|
||||
column++;
|
||||
}
|
||||
current++;
|
||||
char = input[current];
|
||||
}
|
||||
|
||||
let params = [];
|
||||
if (blockType === "server" && char === "(") {
|
||||
current++;
|
||||
column++;
|
||||
let paramString = "";
|
||||
let depth = 1;
|
||||
|
||||
while (current < input.length && depth > 0) {
|
||||
char = input[current];
|
||||
|
||||
if (char === "(") depth++;
|
||||
if (char === ")") depth--;
|
||||
|
||||
if (depth === 0) break;
|
||||
|
||||
paramString += char;
|
||||
if (char === "\n") {
|
||||
line++;
|
||||
column = 1;
|
||||
} else {
|
||||
column++;
|
||||
}
|
||||
current++;
|
||||
}
|
||||
|
||||
current++;
|
||||
column++;
|
||||
|
||||
params = paramString.split(",").map(p => p.trim()).filter(p => p);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`[TokenParser] Server block parameters: ${params.join(", ")}`);
|
||||
}
|
||||
|
||||
char = input[current];
|
||||
while (current < input.length && /\s/.test(char)) {
|
||||
if (char === "\n") {
|
||||
line++;
|
||||
column = 1;
|
||||
} else {
|
||||
column++;
|
||||
}
|
||||
current++;
|
||||
char = input[current];
|
||||
}
|
||||
}
|
||||
|
||||
if (char === "{") {
|
||||
current++;
|
||||
column++;
|
||||
let script = "";
|
||||
let braceCount = 1;
|
||||
|
||||
while (current < input.length && braceCount > 0) {
|
||||
char = input[current];
|
||||
|
||||
if (char === "{") braceCount++;
|
||||
if (char === "}") braceCount--;
|
||||
|
||||
if (braceCount === 0) break;
|
||||
|
||||
script += char;
|
||||
if (char === "\n") {
|
||||
line++;
|
||||
column = 1;
|
||||
} else {
|
||||
column++;
|
||||
}
|
||||
current++;
|
||||
}
|
||||
|
||||
current++;
|
||||
column++;
|
||||
|
||||
tokens.push({
|
||||
type: blockType,
|
||||
value: script.trim(),
|
||||
params: params,
|
||||
line: startLine,
|
||||
column: startColumn,
|
||||
});
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`[TokenParser] ${blockType} block script: "${script.trim().substring(0, 50)}..."`);
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
throw new BlueprintError(
|
||||
`Expected opening brace after @${blockType}${params.length ? '(...)' : ''}`,
|
||||
line,
|
||||
column
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new BlueprintError(
|
||||
`Unknown block type: @${blockType}`,
|
||||
startLine,
|
||||
startColumn
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (/[a-zA-Z]/.test(char)) {
|
||||
let value = "";
|
||||
const startColumn = column;
|
||||
|
|
|
@ -49,6 +49,22 @@ class ButtonElementGenerator {
|
|||
|
||||
let attributes = "";
|
||||
|
||||
const idProp = node.props.find((p) => typeof p === "string" && p.startsWith("id:"));
|
||||
if (idProp) {
|
||||
const idValue = idProp.substring(idProp.indexOf(":") + 1).trim().replace(/^"|"$/g, "");
|
||||
attributes += ` id="${idValue}"`;
|
||||
node.elementId = idValue;
|
||||
if (this.options.debug) {
|
||||
console.log(`[ButtonElementGenerator] Added explicit ID attribute: ${idValue}`);
|
||||
}
|
||||
}
|
||||
else if (node.elementId) {
|
||||
attributes += ` id="${node.elementId}"`;
|
||||
if (this.options.debug) {
|
||||
console.log(`[ButtonElementGenerator] Adding generated ID attribute: ${node.elementId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.parent?.tag === "link") {
|
||||
const linkInfo = this.linkProcessor.processLink(node.parent);
|
||||
attributes += this.linkProcessor.getButtonClickHandler(linkInfo);
|
||||
|
|
|
@ -55,6 +55,23 @@ class InputElementGenerator {
|
|||
attributes = ' type="range"';
|
||||
}
|
||||
|
||||
// Extract and handle ID attribute
|
||||
let idAttr = "";
|
||||
const idProp = node.props.find(p => typeof p === "string" && p.startsWith("id:"));
|
||||
if (idProp) {
|
||||
const idValue = idProp.substring(idProp.indexOf(":") + 1).trim().replace(/^"|"$/g, "");
|
||||
idAttr = ` id="${idValue}"`;
|
||||
node.elementId = idValue;
|
||||
|
||||
// Register as reactive element with the parent generator
|
||||
if (this.parentGenerator && this.parentGenerator.jsGenerator) {
|
||||
this.parentGenerator.jsGenerator.registerReactiveElement(idValue);
|
||||
if (this.options.debug) {
|
||||
console.log(`[InputElementGenerator] Registered checkbox with ID: ${idValue} as reactive`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const valueProp = node.props.find((p) => p.startsWith("value:"));
|
||||
if (valueProp) {
|
||||
const value = valueProp.substring(valueProp.indexOf(":") + 1).trim();
|
||||
|
@ -70,7 +87,7 @@ class InputElementGenerator {
|
|||
|
||||
if (node.children.length > 0) {
|
||||
let html = `<label class="${className}-container">`;
|
||||
html += `<input class="${className}"${attributes}>`;
|
||||
html += `<input class="${className}"${attributes}${idAttr}>`;
|
||||
|
||||
node.children.forEach((child) => {
|
||||
child.parent = node;
|
||||
|
@ -80,7 +97,7 @@ class InputElementGenerator {
|
|||
html += `</label>`;
|
||||
return html;
|
||||
} else {
|
||||
return `<input class="${className}"${attributes}>`;
|
||||
return `<input class="${className}"${attributes}${idAttr}>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
239
lib/generators/JavaScriptGenerator.js
Normal file
239
lib/generators/JavaScriptGenerator.js
Normal file
|
@ -0,0 +1,239 @@
|
|||
const StringUtils = require("../utils/StringUtils");
|
||||
|
||||
/**
|
||||
* Generates JavaScript code for client and server blocks with reactive data handling.
|
||||
*/
|
||||
class JavaScriptGenerator {
|
||||
/**
|
||||
* Creates a new JavaScript generator.
|
||||
* @param {Object} options - Options for the generator
|
||||
* @param {ServerCodeGenerator} [serverGenerator] - Server code generator instance
|
||||
*/
|
||||
constructor(options = {}, serverGenerator = null) {
|
||||
this.options = options;
|
||||
this.clientScripts = new Map();
|
||||
this.serverScripts = [];
|
||||
this.reactiveElements = new Set();
|
||||
this.serverGenerator = serverGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the server code generator instance
|
||||
* @param {ServerCodeGenerator} serverGenerator - Server code generator instance
|
||||
*/
|
||||
setServerGenerator(serverGenerator) {
|
||||
this.serverGenerator = serverGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a client-side script to be executed when an element is clicked.
|
||||
* @param {string} elementId - The ID of the element to attach the event to
|
||||
* @param {string} code - The JavaScript code to execute
|
||||
*/
|
||||
addClientScript(elementId, code) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[JavaScriptGenerator] Adding client script for element ${elementId}`);
|
||||
}
|
||||
this.clientScripts.set(elementId, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a server-side script to be executed on the server.
|
||||
* @param {string} elementId - The ID of the element that triggers the server action
|
||||
* @param {string} code - The JavaScript code to execute
|
||||
* @param {Array<string>} params - The input parameters to retrieve from the client
|
||||
*/
|
||||
addServerScript(elementId, code, params = []) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[JavaScriptGenerator] Adding server script for element ${elementId}`);
|
||||
if (params.length > 0) {
|
||||
console.log(`[JavaScriptGenerator] Script parameters: ${params.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.serverScripts.push({
|
||||
elementId,
|
||||
code,
|
||||
params
|
||||
});
|
||||
|
||||
if (this.serverGenerator) {
|
||||
this.serverGenerator.addServerRoute(elementId, code, params);
|
||||
}
|
||||
|
||||
this.clientScripts.set(elementId, `_bp_serverAction_${elementId}(e);`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an element as reactive, meaning it will have state management functions
|
||||
* @param {string} elementId - The ID of the element to make reactive
|
||||
*/
|
||||
registerReactiveElement(elementId) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[JavaScriptGenerator] Registering reactive element: ${elementId}`);
|
||||
}
|
||||
this.reactiveElements.add(elementId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique element ID.
|
||||
* @returns {string} - A unique element ID with bp_ prefix
|
||||
*/
|
||||
generateElementId() {
|
||||
return StringUtils.generateRandomId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the reactive store and helper functions for state management
|
||||
* @returns {string} - JavaScript code for reactive functionality
|
||||
*/
|
||||
generateReactiveStore() {
|
||||
if (this.reactiveElements.size === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `
|
||||
const _bp_store = {
|
||||
listeners: new Map(),
|
||||
subscribe: function(id, callback) {
|
||||
if (!this.listeners.has(id)) {
|
||||
this.listeners.set(id, []);
|
||||
}
|
||||
this.listeners.get(id).push(callback);
|
||||
},
|
||||
notify: function(id, newValue) {
|
||||
if (this.listeners.has(id)) {
|
||||
this.listeners.get(id).forEach(callback => callback(newValue));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function _bp_makeElementReactive(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) {
|
||||
console.log(\`[Blueprint] Element with ID \${id} not found\`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
element: element,
|
||||
get value() {
|
||||
return element.textContent;
|
||||
},
|
||||
set: function(newValue) {
|
||||
const valueString = String(newValue);
|
||||
element.textContent = valueString;
|
||||
_bp_store.notify(id, valueString);
|
||||
return this;
|
||||
},
|
||||
setNumber: function(num) {
|
||||
const valueString = String(Number(num));
|
||||
element.textContent = valueString;
|
||||
_bp_store.notify(id, valueString);
|
||||
return this;
|
||||
},
|
||||
setHtml: function(html) {
|
||||
element.innerHTML = html;
|
||||
_bp_store.notify(id, html);
|
||||
return this;
|
||||
},
|
||||
setStyle: function(property, value) {
|
||||
element.style[property] = value;
|
||||
return this;
|
||||
},
|
||||
setClass: function(className, add = true) {
|
||||
if (add) {
|
||||
element.classList.add(className);
|
||||
} else {
|
||||
element.classList.remove(className);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
on: function(event, callback) {
|
||||
element.addEventListener(event, callback);
|
||||
return this;
|
||||
},
|
||||
subscribe: function(callback) {
|
||||
_bp_store.subscribe(id, callback);
|
||||
return this;
|
||||
},
|
||||
get textValue() {
|
||||
return element.textContent;
|
||||
},
|
||||
get numberValue() {
|
||||
return Number(element.textContent);
|
||||
},
|
||||
get booleanValue() {
|
||||
const text = element.textContent.toLowerCase();
|
||||
return text === 'true' || text === '1' || text === 'yes';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize reactive elements`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates initialization code for reactive elements
|
||||
* @returns {string} - JavaScript initialization code for reactive elements
|
||||
*/
|
||||
generateReactiveElementInit() {
|
||||
if (this.reactiveElements.size === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const initCode = Array.from(this.reactiveElements)
|
||||
.map(id => `const ${id} = _bp_makeElementReactive('${id}');`)
|
||||
.join('\n ');
|
||||
|
||||
return `
|
||||
${initCode}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates all client-side JavaScript code.
|
||||
* @returns {string} - The generated JavaScript code
|
||||
*/
|
||||
generateClientScripts() {
|
||||
if (this.clientScripts.size === 0 && this.reactiveElements.size === 0 && !this.serverGenerator) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let initCode = '';
|
||||
if (this.reactiveElements.size > 0) {
|
||||
initCode = this.generateReactiveElementInit();
|
||||
}
|
||||
|
||||
let scripts = '';
|
||||
this.clientScripts.forEach((code, elementId) => {
|
||||
scripts += ` document.getElementById('${elementId}').addEventListener('click', function(e) {
|
||||
${code}
|
||||
});\n`;
|
||||
});
|
||||
|
||||
let serverClientCode = '';
|
||||
if (this.serverGenerator) {
|
||||
serverClientCode = this.serverGenerator.generateClientAPICalls();
|
||||
}
|
||||
|
||||
return `<script>
|
||||
${this.generateReactiveStore()}
|
||||
${serverClientCode}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
${initCode}
|
||||
|
||||
${scripts}});
|
||||
</script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all server-side scripts.
|
||||
* @returns {Array<Object>} - Array of server-side script objects
|
||||
*/
|
||||
getServerScripts() {
|
||||
return this.serverScripts;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JavaScriptGenerator;
|
236
lib/generators/ServerCodeGenerator.js
Normal file
236
lib/generators/ServerCodeGenerator.js
Normal file
|
@ -0,0 +1,236 @@
|
|||
const crypto = require('crypto');
|
||||
const StringUtils = require("../utils/StringUtils");
|
||||
|
||||
/**
|
||||
* Generates server-side Express.js routes for server blocks.
|
||||
*/
|
||||
class ServerCodeGenerator {
|
||||
/**
|
||||
* Creates a new server code generator.
|
||||
* @param {Object} options - Options for the generator
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.serverRoutes = new Map();
|
||||
this.hasServerCode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a server-side route to be executed when requested.
|
||||
* @param {string} elementId - The ID of the element that triggers the route
|
||||
* @param {string} code - The JavaScript code to execute
|
||||
* @param {Array<string>} params - The input parameters to retrieve from the client
|
||||
*/
|
||||
addServerRoute(elementId, code, params = []) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[ServerCodeGenerator] Adding server route for element ${elementId}`);
|
||||
if (params.length > 0) {
|
||||
console.log(`[ServerCodeGenerator] Route parameters: ${params.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = this.generateEndpointPath(elementId);
|
||||
|
||||
this.serverRoutes.set(elementId, {
|
||||
endpoint,
|
||||
code,
|
||||
params
|
||||
});
|
||||
|
||||
this.hasServerCode = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique endpoint path for a server route.
|
||||
* @param {string} elementId - The element ID for the route
|
||||
* @returns {string} - A unique endpoint path
|
||||
*/
|
||||
generateEndpointPath(elementId) {
|
||||
const hash = crypto.createHash('sha256')
|
||||
.update(elementId + Math.random().toString())
|
||||
.digest('hex')
|
||||
.substring(0, 12);
|
||||
|
||||
return `/api/${StringUtils.toKebabCase(elementId)}-${hash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates client-side JavaScript for making API calls to server routes.
|
||||
* @returns {string} - JavaScript code for making API calls
|
||||
*/
|
||||
generateClientAPICalls() {
|
||||
if (this.serverRoutes.size === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let apiCode = `
|
||||
const _bp_api = {
|
||||
post: async function(url, data) {
|
||||
try {
|
||||
const serverPort = window.blueprintServerPort || 3001;
|
||||
|
||||
const fullUrl = \`http://\${window.location.hostname}:\${serverPort}\${url}\`;
|
||||
|
||||
const response = await fetch(fullUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`API request failed: \${response.status}\`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('[Blueprint API]', error);
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
`;
|
||||
|
||||
|
||||
this.serverRoutes.forEach((route, elementId) => {
|
||||
const { endpoint, params } = route;
|
||||
|
||||
apiCode += `async function _bp_serverAction_${elementId}(e) {
|
||||
const data = {};
|
||||
${params.map(param => ` const ${param}_element = document.getElementById('${param}');
|
||||
if (${param}_element) {
|
||||
console.log('Found element: ${param}', ${param}_element);
|
||||
if (${param}_element.type === 'checkbox') {
|
||||
data.${param} = ${param}_element.checked;
|
||||
console.log('Checkbox ${param} value:', ${param}_element.checked);
|
||||
} else if (${param}_element.type === 'radio') {
|
||||
data.${param} = ${param}_element.checked;
|
||||
} else if (${param}_element.value !== undefined) {
|
||||
data.${param} = ${param}_element.value;
|
||||
} else {
|
||||
data.${param} = ${param}_element.textContent;
|
||||
}
|
||||
} else {
|
||||
console.error('[Blueprint] Element with ID ${param} not found');
|
||||
data.${param} = null;
|
||||
}`).join('\n')}
|
||||
|
||||
console.log('Submitting data:', data);
|
||||
|
||||
try {
|
||||
const result = await _bp_api.post('${endpoint}', data);
|
||||
console.log('[Blueprint API] Server response:', result);
|
||||
|
||||
if (result && typeof result === 'object') {
|
||||
Object.keys(result).forEach(key => {
|
||||
if (window[key] && typeof window[key].set === 'function') {
|
||||
window[key].set(result[key]);
|
||||
}
|
||||
else {
|
||||
const element = document.getElementById(key);
|
||||
if (element) {
|
||||
if (element.tagName.toLowerCase() === 'input') {
|
||||
element.value = result[key];
|
||||
} else {
|
||||
element.textContent = result[key];
|
||||
}
|
||||
console.log(\`[Blueprint API] Updated element #\${key} with value: \${result[key]}\`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[Blueprint API] Error in server action:', error);
|
||||
}
|
||||
}\n`;
|
||||
});
|
||||
|
||||
return apiCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates Express.js server code for all registered server routes.
|
||||
* @returns {string} - Express.js server code
|
||||
*/
|
||||
generateServerCode() {
|
||||
if (this.serverRoutes.size === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let serverCode = `
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const cors = require('cors');
|
||||
|
||||
function createBlueprintApiServer(port = 3001) {
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.use((req, res, next) => {
|
||||
console.log(\`[\${new Date().toISOString()}] \${req.method} \${req.url}\`);
|
||||
next();
|
||||
});
|
||||
|
||||
`;
|
||||
|
||||
this.serverRoutes.forEach((route, elementId) => {
|
||||
const { endpoint, code, params } = route;
|
||||
|
||||
serverCode += `
|
||||
app.post('${endpoint}', async (req, res) => {
|
||||
try {
|
||||
${params.map(param => {
|
||||
return `const ${param} = req.body.${param} !== undefined ? req.body.${param} : null;
|
||||
if (${param} === null) {
|
||||
console.error(\`Missing parameter: ${param}\`);
|
||||
}`;
|
||||
}).join('\n ')}
|
||||
|
||||
let result;
|
||||
try {
|
||||
${code}
|
||||
} catch (error) {
|
||||
console.error(\`Error in server block \${error.message}\`);
|
||||
return res.status(500).json({ error: error.message });
|
||||
}
|
||||
|
||||
return res.json(result || {});
|
||||
} catch (error) {
|
||||
console.error(\`Error processing request: \${error.message}\`);
|
||||
return res.status(500).json({ error: error.message });
|
||||
}
|
||||
});`;
|
||||
});
|
||||
|
||||
serverCode += `
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(\`Blueprint API server running at http://localhost:\${port}\`);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
module.exports = createBlueprintApiServer;
|
||||
`;
|
||||
|
||||
return serverCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is any server code to generate.
|
||||
* @returns {boolean} - Whether there is server code
|
||||
*/
|
||||
hasServerCodeToGenerate() {
|
||||
return this.hasServerCode;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServerCodeGenerator;
|
|
@ -54,6 +54,22 @@ class StandardElementGenerator {
|
|||
|
||||
let attributes = "";
|
||||
|
||||
const idProp = node.props.find((p) => typeof p === "string" && p.startsWith("id:"));
|
||||
if (idProp) {
|
||||
const idValue = idProp.substring(idProp.indexOf(":") + 1).trim().replace(/^"|"$/g, "");
|
||||
attributes += ` id="${idValue}"`;
|
||||
node.elementId = idValue;
|
||||
if (this.options.debug) {
|
||||
console.log(`[StandardElementGenerator] Added explicit ID attribute: ${idValue}`);
|
||||
}
|
||||
}
|
||||
else if (node.elementId) {
|
||||
attributes += ` id="${node.elementId}"`;
|
||||
if (this.options.debug) {
|
||||
console.log(`[StandardElementGenerator] Adding generated ID attribute: ${node.elementId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.props.find((p) => typeof p === "string" && p.startsWith("data-"))) {
|
||||
const dataProps = node.props.filter(
|
||||
(p) => typeof p === "string" && p.startsWith("data-")
|
||||
|
|
|
@ -538,7 +538,7 @@ const ELEMENT_MAPPINGS = {
|
|||
},
|
||||
card: {
|
||||
tag: "div",
|
||||
defaultProps: ["raised", "card"],
|
||||
defaultProps: ["card"],
|
||||
},
|
||||
grid: {
|
||||
tag: "div",
|
||||
|
|
291
lib/server.js
291
lib/server.js
|
@ -11,6 +11,7 @@ class BlueprintServer {
|
|||
this.wsInstance = expressWs(this.app);
|
||||
this.options = {
|
||||
port: 3000,
|
||||
apiPort: 3001,
|
||||
srcDir: "./src",
|
||||
outDir: "./dist",
|
||||
liveReload: false,
|
||||
|
@ -19,6 +20,8 @@ class BlueprintServer {
|
|||
};
|
||||
this.clients = new Map();
|
||||
this.filesWithErrors = new Set();
|
||||
this.apiServers = new Map();
|
||||
this.apiPorts = new Map();
|
||||
this.setupServer();
|
||||
if (this.options.liveReload) {
|
||||
const watcher = chokidar.watch([], {
|
||||
|
@ -51,6 +54,27 @@ class BlueprintServer {
|
|||
);
|
||||
}
|
||||
|
||||
async buildFile(filePath) {
|
||||
const relativePath = path.relative(this.options.srcDir, filePath);
|
||||
const outputPath = path.join(
|
||||
this.options.outDir,
|
||||
relativePath.replace(/\.bp$/, ".html")
|
||||
);
|
||||
this.ensureDirectoryExistence(outputPath);
|
||||
|
||||
const builder = new BlueprintBuilder({
|
||||
minified: this.options.minified,
|
||||
debug: this.options.debug,
|
||||
});
|
||||
const buildResult = builder.build(filePath, path.dirname(outputPath));
|
||||
|
||||
if (buildResult.success && buildResult.hasServerCode) {
|
||||
this.startApiServer(relativePath.replace(/\.bp$/, ""));
|
||||
}
|
||||
|
||||
return buildResult;
|
||||
}
|
||||
|
||||
async buildAll() {
|
||||
this.log("INFO", "Building all Blueprint files...", "lightGray");
|
||||
if (fs.existsSync(this.options.outDir)) {
|
||||
|
@ -70,8 +94,17 @@ class BlueprintServer {
|
|||
);
|
||||
this.ensureDirectoryExistence(outputPath);
|
||||
|
||||
const builder = new BlueprintBuilder({ minified: this.options.minified });
|
||||
const builder = new BlueprintBuilder({
|
||||
minified: this.options.minified,
|
||||
debug: this.options.debug
|
||||
});
|
||||
const result = builder.build(file, path.dirname(outputPath));
|
||||
|
||||
if (result.success && result.hasServerCode) {
|
||||
const fileName = relativePath.replace(/\.bp$/, "");
|
||||
this.startApiServer(fileName);
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
success = false;
|
||||
errors.push({ file, errors: result.errors });
|
||||
|
@ -138,8 +171,19 @@ class BlueprintServer {
|
|||
fs.readFile(htmlPath, "utf8", (err, data) => {
|
||||
if (err) return next();
|
||||
let html = data;
|
||||
|
||||
const filePath = req.path.endsWith(".html")
|
||||
? req.path.slice(0, -5)
|
||||
: req.path === "/"
|
||||
? "index"
|
||||
: req.path.replace(/^\//, "");
|
||||
|
||||
const apiPort = this.apiPorts.get(filePath) || this.options.apiPort;
|
||||
|
||||
const script = `
|
||||
<script>
|
||||
window.blueprintServerPort = ${apiPort};
|
||||
|
||||
(function() {
|
||||
let currentPage = window.location.pathname.replace(/^\\//, '') || 'index.html';
|
||||
console.log('Current page:', currentPage);
|
||||
|
@ -240,7 +284,33 @@ class BlueprintServer {
|
|||
|
||||
const htmlPath = path.join(this.options.outDir, req.path + ".html");
|
||||
if (fs.existsSync(htmlPath)) {
|
||||
res.sendFile(htmlPath);
|
||||
fs.readFile(htmlPath, "utf8", (err, data) => {
|
||||
if (err) return res.sendFile(htmlPath);
|
||||
|
||||
let html = data;
|
||||
|
||||
const filePath = req.path === "/"
|
||||
? "index"
|
||||
: req.path.replace(/^\//, "");
|
||||
|
||||
const apiPort = this.apiPorts.get(filePath) || this.options.apiPort;
|
||||
|
||||
if (!html.includes('window.blueprintServerPort =')) {
|
||||
const script = `
|
||||
<script>
|
||||
// Inject the Blueprint server port for API calls
|
||||
window.blueprintServerPort = ${apiPort};
|
||||
</script>
|
||||
`;
|
||||
html = html.replace("</head>", script + "</head>");
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
res.setHeader("Expires", "0");
|
||||
return res.send(html);
|
||||
});
|
||||
} else if (req.path === "/") {
|
||||
const pages = fs
|
||||
.readdirSync(this.options.outDir)
|
||||
|
@ -311,83 +381,35 @@ class BlueprintServer {
|
|||
}
|
||||
|
||||
setupWatcher(watcher) {
|
||||
watcher.on("change", async (filepath) => {
|
||||
if (filepath.endsWith(".bp")) {
|
||||
this.log("INFO", `File ${filepath} has been changed`, "blue");
|
||||
try {
|
||||
const builder = new BlueprintBuilder({
|
||||
minified: this.options.minified,
|
||||
debug: this.options.debug,
|
||||
});
|
||||
const relativePath = path.relative(this.options.srcDir, filepath);
|
||||
const outputPath = path.join(
|
||||
this.options.outDir,
|
||||
relativePath.replace(/\.bp$/, ".html")
|
||||
);
|
||||
this.ensureDirectoryExistence(outputPath);
|
||||
const result = builder.build(filepath, path.dirname(outputPath));
|
||||
|
||||
if (result.success) {
|
||||
this.log("SUCCESS", "Rebuilt successfully", "green");
|
||||
|
||||
this.filesWithErrors.delete(filepath);
|
||||
|
||||
const htmlFile = relativePath.replace(/\.bp$/, ".html");
|
||||
const htmlPath = path.join(this.options.outDir, htmlFile);
|
||||
|
||||
try {
|
||||
const newContent = fs.readFileSync(htmlPath, "utf8");
|
||||
|
||||
for (const [client, page] of this.clients.entries()) {
|
||||
if (
|
||||
page === htmlFile.replace(/\\/g, "/") &&
|
||||
client.readyState === 1
|
||||
) {
|
||||
try {
|
||||
client.send(
|
||||
JSON.stringify({
|
||||
type: "reload",
|
||||
content: newContent,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
this.log("ERROR", "Error sending content:", "red");
|
||||
this.clients.delete(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("ERROR", "Error reading new content:", "red");
|
||||
}
|
||||
} else {
|
||||
this.filesWithErrors.add(filepath);
|
||||
this.log("ERROR", `Build failed: ${result.errors.map(e => e.message).join(", ")}`, "red");
|
||||
this.log("INFO", "Waiting for next file change...", "orange");
|
||||
|
||||
for (const [client, page] of this.clients.entries()) {
|
||||
const htmlFile = relativePath.replace(/\.bp$/, ".html");
|
||||
if (
|
||||
page === htmlFile.replace(/\\/g, "/") &&
|
||||
client.readyState === 1
|
||||
) {
|
||||
try {
|
||||
client.send(
|
||||
JSON.stringify({
|
||||
type: "buildError",
|
||||
errors: result.errors,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
this.log("ERROR", "Error sending error notification:", "red");
|
||||
this.clients.delete(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("ERROR", "Unexpected error during build:", "red");
|
||||
this.filesWithErrors.add(filepath);
|
||||
watcher.on("change", async (filePath) => {
|
||||
if (!filePath.endsWith(".bp")) return;
|
||||
this.log("INFO", `File changed: ${filePath}`, "blue");
|
||||
|
||||
const result = await this.buildFile(filePath);
|
||||
|
||||
if (result.success) {
|
||||
this.log("SUCCESS", `Rebuilt ${filePath}`, "green");
|
||||
this.filesWithErrors.delete(filePath);
|
||||
|
||||
if (result.hasServerCode) {
|
||||
const relativePath = path.relative(this.options.srcDir, filePath);
|
||||
const fileName = relativePath.replace(/\.bp$/, "");
|
||||
this.startApiServer(fileName);
|
||||
}
|
||||
|
||||
if (this.options.liveReload) {
|
||||
this.notifyClients(filePath);
|
||||
}
|
||||
} else {
|
||||
this.log("ERROR", `Build failed for ${filePath}`, "red");
|
||||
result.errors.forEach((err) => {
|
||||
this.log(
|
||||
"ERROR",
|
||||
`${err.type} at line ${err.line}, column ${err.column}: ${err.message}`,
|
||||
"red"
|
||||
);
|
||||
});
|
||||
this.filesWithErrors.add(filePath);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -468,6 +490,119 @@ class BlueprintServer {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
startApiServer(fileName) {
|
||||
const serverFilePath = path.join(this.options.outDir, 'server', `${fileName}-server.js`);
|
||||
|
||||
if (!fs.existsSync(serverFilePath)) {
|
||||
this.log("ERROR", `API server file not found: ${serverFilePath}`, "red");
|
||||
return;
|
||||
}
|
||||
|
||||
let apiPort;
|
||||
|
||||
if (this.apiPorts.has(fileName)) {
|
||||
apiPort = this.apiPorts.get(fileName);
|
||||
this.log("INFO", `Reusing port ${apiPort} for ${fileName}`, "blue");
|
||||
} else {
|
||||
apiPort = this.options.apiPort;
|
||||
this.options.apiPort++;
|
||||
this.log("INFO", `Assigning new port ${apiPort} for ${fileName}`, "blue");
|
||||
}
|
||||
|
||||
const startNewServer = () => {
|
||||
try {
|
||||
delete require.cache[require.resolve(path.resolve(serverFilePath))];
|
||||
|
||||
const createApiServer = require(path.resolve(serverFilePath));
|
||||
const apiServer = createApiServer(apiPort);
|
||||
|
||||
this.apiServers.set(fileName, apiServer);
|
||||
this.apiPorts.set(fileName, apiPort);
|
||||
|
||||
this.log("SUCCESS", `API server started for ${fileName} on port ${apiPort}`, "green");
|
||||
} catch (error) {
|
||||
this.log("ERROR", `Failed to start API server: ${error.message}`, "red");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.apiServers.has(fileName)) {
|
||||
const existingServer = this.apiServers.get(fileName);
|
||||
this.log("INFO", `Stopping previous API server for ${fileName}`, "blue");
|
||||
|
||||
try {
|
||||
if (existingServer && typeof existingServer.close === 'function') {
|
||||
existingServer.close(() => {
|
||||
this.log("INFO", `Previous server closed, starting new one`, "blue");
|
||||
setTimeout(startNewServer, 300);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingServer && existingServer.server && existingServer.server.close) {
|
||||
existingServer.server.close(() => {
|
||||
this.log("INFO", `Previous server closed, starting new one`, "blue");
|
||||
setTimeout(startNewServer, 300);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.log("WARNING", `Could not properly close previous server, waiting longer`, "orange");
|
||||
setTimeout(startNewServer, 1000);
|
||||
} catch (err) {
|
||||
this.log("WARNING", `Error closing previous server: ${err.message}`, "orange");
|
||||
setTimeout(startNewServer, 2000);
|
||||
}
|
||||
} else {
|
||||
startNewServer();
|
||||
}
|
||||
}
|
||||
|
||||
notifyClients(filePath) {
|
||||
const relativePath = path.relative(this.options.srcDir, filePath);
|
||||
const htmlFile = relativePath.replace(/\.bp$/, ".html");
|
||||
const htmlPath = path.join(this.options.outDir, htmlFile);
|
||||
const fileName = relativePath.replace(/\.bp$/, "");
|
||||
|
||||
try {
|
||||
let newContent = fs.readFileSync(htmlPath, "utf8");
|
||||
|
||||
const apiPort = this.apiPorts.get(fileName) || this.options.apiPort;
|
||||
|
||||
if (!newContent.includes('window.blueprintServerPort =')) {
|
||||
newContent = newContent.replace(
|
||||
'<head>',
|
||||
`<head>
|
||||
<script>
|
||||
window.blueprintServerPort = ${apiPort};
|
||||
</script>`
|
||||
);
|
||||
}
|
||||
|
||||
for (const [client, page] of this.clients.entries()) {
|
||||
if (
|
||||
page === htmlFile.replace(/\\/g, "/") &&
|
||||
client.readyState === 1
|
||||
) {
|
||||
try {
|
||||
client.send(
|
||||
JSON.stringify({
|
||||
type: "reload",
|
||||
content: newContent,
|
||||
})
|
||||
);
|
||||
this.log("INFO", `Sent update to client for ${htmlFile}`, "blue");
|
||||
} catch (error) {
|
||||
this.log("ERROR", "Error sending content:", "red");
|
||||
this.clients.delete(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("ERROR", `Error reading new content from ${htmlPath}: ${error.message}`, "red");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlueprintServer;
|
||||
|
|
|
@ -52,8 +52,23 @@ const safeStringify = (obj) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random ID string suitable for use as an HTML element ID.
|
||||
* @param {number} [length=8] - The length of the random ID
|
||||
* @returns {string} - A random alphanumeric ID with bp_ prefix
|
||||
*/
|
||||
const generateRandomId = (length = 8) => {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = 'bp_';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
escapeHTML,
|
||||
toKebabCase,
|
||||
safeStringify
|
||||
safeStringify,
|
||||
generateRandomId
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue