beta/code-blocks #1
19 changed files with 756 additions and 94 deletions
Binary file not shown.
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
||||||
{"root":["../client/src/extension.ts","../server/src/server.ts"],"version":"5.7.3"}
|
{"root":["../client/src/extension.ts","../server/src/server.ts"],"version":"5.8.2"}
|
File diff suppressed because one or more lines are too long
|
@ -38,6 +38,8 @@ const elements = [
|
||||||
'card', 'badge', 'alert', 'tooltip', 'input', 'textarea', 'select',
|
'card', 'badge', 'alert', 'tooltip', 'input', 'textarea', 'select',
|
||||||
'checkbox', 'radio', 'switch', 'list', 'table', 'progress', 'slider'
|
'checkbox', 'radio', 'switch', 'list', 'table', 'progress', 'slider'
|
||||||
];
|
];
|
||||||
|
// Script block types
|
||||||
|
const scriptBlocks = ['client', 'server'];
|
||||||
// Single instance elements
|
// Single instance elements
|
||||||
const singleElements = ['page', 'navbar'];
|
const singleElements = ['page', 'navbar'];
|
||||||
// Blueprint properties
|
// Blueprint properties
|
||||||
|
@ -104,6 +106,65 @@ connection.onCompletion((textDocumentPosition) => {
|
||||||
}]
|
}]
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
// Check for @client or @server block completion
|
||||||
|
if (linePrefix.trim() === '@' || linePrefix.trim().startsWith('@')) {
|
||||||
|
return scriptBlocks.map(blockType => ({
|
||||||
|
label: `@${blockType}`,
|
||||||
|
kind: node_1.CompletionItemKind.Snippet,
|
||||||
|
insertText: `@${blockType} {\n $1\n}`,
|
||||||
|
insertTextFormat: node_1.InsertTextFormat.Snippet,
|
||||||
|
documentation: `Create a ${blockType} script block`
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// After an @client or @server block opening brace, suggest JS snippets
|
||||||
|
const scriptBlockMatch = /@(client|server)\s*{\s*$/.exec(linePrefix);
|
||||||
|
if (scriptBlockMatch) {
|
||||||
|
const blockType = scriptBlockMatch[1];
|
||||||
|
const jsSnippets = [];
|
||||||
|
if (blockType === 'client') {
|
||||||
|
jsSnippets.push({
|
||||||
|
label: 'element.set',
|
||||||
|
kind: node_1.CompletionItemKind.Method,
|
||||||
|
insertText: '${1:elementId}.set("${2:new value}");',
|
||||||
|
insertTextFormat: node_1.InsertTextFormat.Snippet,
|
||||||
|
documentation: 'Set the content of an element'
|
||||||
|
}, {
|
||||||
|
label: 'console.log',
|
||||||
|
kind: node_1.CompletionItemKind.Method,
|
||||||
|
insertText: 'console.log("${1:message}");',
|
||||||
|
insertTextFormat: node_1.InsertTextFormat.Snippet,
|
||||||
|
documentation: 'Log a message to the console'
|
||||||
|
}, {
|
||||||
|
label: 'DOM event handling',
|
||||||
|
kind: node_1.CompletionItemKind.Snippet,
|
||||||
|
insertText: 'e.preventDefault();\n${1}',
|
||||||
|
insertTextFormat: node_1.InsertTextFormat.Snippet,
|
||||||
|
documentation: 'Prevent default action of event'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (blockType === 'server') {
|
||||||
|
jsSnippets.push({
|
||||||
|
label: 'element.set',
|
||||||
|
kind: node_1.CompletionItemKind.Method,
|
||||||
|
insertText: '${1:elementId}.set(${2:newValue});',
|
||||||
|
insertTextFormat: node_1.InsertTextFormat.Snippet,
|
||||||
|
documentation: 'Update element value from server'
|
||||||
|
}, {
|
||||||
|
label: 'element.value',
|
||||||
|
kind: node_1.CompletionItemKind.Property,
|
||||||
|
insertText: 'const value = ${1:elementId}.value;',
|
||||||
|
insertTextFormat: node_1.InsertTextFormat.Snippet,
|
||||||
|
documentation: 'Get the current value of an element'
|
||||||
|
}, {
|
||||||
|
label: 'fetch data',
|
||||||
|
kind: node_1.CompletionItemKind.Snippet,
|
||||||
|
insertText: 'const response = await fetch("${1:url}");\nconst data = await response.json();\n${2}',
|
||||||
|
insertTextFormat: node_1.InsertTextFormat.Snippet,
|
||||||
|
documentation: 'Fetch data from an API'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return jsSnippets;
|
||||||
|
}
|
||||||
// Inside page block
|
// Inside page block
|
||||||
if (text.includes('page {') && !text.includes('}')) {
|
if (text.includes('page {') && !text.includes('}')) {
|
||||||
return pageProperties.map(prop => ({
|
return pageProperties.map(prop => ({
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -58,6 +58,11 @@ const elements = [
|
||||||
'checkbox', 'radio', 'switch', 'list', 'table', 'progress', 'slider'
|
'checkbox', 'radio', 'switch', 'list', 'table', 'progress', 'slider'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Script blocks
|
||||||
|
const scriptBlocks = [
|
||||||
|
'client', 'server'
|
||||||
|
];
|
||||||
|
|
||||||
// Single instance elements
|
// Single instance elements
|
||||||
const singleElements = ['page', 'navbar'];
|
const singleElements = ['page', 'navbar'];
|
||||||
|
|
||||||
|
@ -75,6 +80,9 @@ const properties = [
|
||||||
// Page configuration properties
|
// Page configuration properties
|
||||||
const pageProperties = ['title', 'description', 'keywords', 'author'];
|
const pageProperties = ['title', 'description', 'keywords', 'author'];
|
||||||
|
|
||||||
|
// ID attribute suggestion - using underscore format
|
||||||
|
const idAttributeTemplate = 'id:$1_$2';
|
||||||
|
|
||||||
// Container elements that can have children
|
// Container elements that can have children
|
||||||
const containerElements = [
|
const containerElements = [
|
||||||
'horizontal', 'vertical', 'section', 'grid', 'navbar',
|
'horizontal', 'vertical', 'section', 'grid', 'navbar',
|
||||||
|
@ -114,6 +122,19 @@ connection.onCompletion(
|
||||||
const line = lines[position.line];
|
const line = lines[position.line];
|
||||||
const linePrefix = line.slice(0, position.character);
|
const linePrefix = line.slice(0, position.character);
|
||||||
|
|
||||||
|
// Suggest script blocks after @ symbol
|
||||||
|
if (linePrefix.trim().endsWith('@')) {
|
||||||
|
return scriptBlocks.map(block => ({
|
||||||
|
label: `@${block}`,
|
||||||
|
kind: CompletionItemKind.Snippet,
|
||||||
|
insertText: `@${block} {\n $1\n}`,
|
||||||
|
insertTextFormat: InsertTextFormat.Snippet,
|
||||||
|
documentation: block === 'client' ?
|
||||||
|
'Create a client-side JavaScript block that runs when the element is clicked. The "e" event object is available.' :
|
||||||
|
'Create a server-side JavaScript block that runs on the server.'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a template completion trigger
|
// Check if this is a template completion trigger
|
||||||
if (linePrefix.trim() === '!') {
|
if (linePrefix.trim() === '!') {
|
||||||
return [{
|
return [{
|
||||||
|
@ -145,13 +166,22 @@ connection.onCompletion(
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// After an opening parenthesis, suggest properties
|
// After an opening parenthesis, suggest properties including ID with underscore format
|
||||||
if (linePrefix.trim().endsWith('(')) {
|
if (linePrefix.trim().endsWith('(')) {
|
||||||
return properties.map(prop => ({
|
return [
|
||||||
label: prop,
|
...properties.map(prop => ({
|
||||||
kind: CompletionItemKind.Property,
|
label: prop,
|
||||||
documentation: `Apply ${prop} property`
|
kind: CompletionItemKind.Property,
|
||||||
}));
|
documentation: `Apply ${prop} property`
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
label: 'id',
|
||||||
|
kind: CompletionItemKind.Property,
|
||||||
|
insertText: idAttributeTemplate,
|
||||||
|
insertTextFormat: InsertTextFormat.Snippet,
|
||||||
|
documentation: 'Add an ID to the element (use underscores instead of hyphens for JavaScript compatibility)'
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// After a container element's opening brace, suggest child elements
|
// After a container element's opening brace, suggest child elements
|
||||||
|
@ -173,6 +203,26 @@ connection.onCompletion(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include client/server block suggestions for interactive elements
|
||||||
|
if (['button', 'button-light', 'button-secondary', 'button-compact'].includes(parentElement)) {
|
||||||
|
return [
|
||||||
|
...suggestedElements.map(element => ({
|
||||||
|
label: element,
|
||||||
|
kind: CompletionItemKind.Class,
|
||||||
|
insertText: `${element} {\n $1\n}`,
|
||||||
|
insertTextFormat: InsertTextFormat.Snippet,
|
||||||
|
documentation: `Create a ${element} block inside ${parentElement}`
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
label: '@client',
|
||||||
|
kind: CompletionItemKind.Snippet,
|
||||||
|
insertText: `@client {\n $1\n}`,
|
||||||
|
insertTextFormat: InsertTextFormat.Snippet,
|
||||||
|
documentation: 'Create a client-side JavaScript block that runs when the element is clicked. The "e" event object is available.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return suggestedElements.map(element => ({
|
return suggestedElements.map(element => ({
|
||||||
label: element,
|
label: element,
|
||||||
kind: CompletionItemKind.Class,
|
kind: CompletionItemKind.Class,
|
||||||
|
@ -182,6 +232,27 @@ connection.onCompletion(
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inside interactive elements, suggest @client blocks
|
||||||
|
const interactiveElementMatch = /\b(button|button-light|button-secondary|button-compact|input|textarea|select|checkbox|radio|switch)\s*(?:\([^)]*\))?\s*{\s*$/.exec(linePrefix);
|
||||||
|
if (interactiveElementMatch) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: '@client',
|
||||||
|
kind: CompletionItemKind.Snippet,
|
||||||
|
insertText: `@client {\n $1\n}`,
|
||||||
|
insertTextFormat: InsertTextFormat.Snippet,
|
||||||
|
documentation: 'Create a client-side JavaScript block that runs when the element is clicked. The "e" event object is available.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'text',
|
||||||
|
kind: CompletionItemKind.Class,
|
||||||
|
insertText: `"$1"`,
|
||||||
|
insertTextFormat: InsertTextFormat.Snippet,
|
||||||
|
documentation: 'Add text content to the element'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Get available single instance elements
|
// Get available single instance elements
|
||||||
const availableSingleElements = singleElements.filter(element => !elementExists(text, element));
|
const availableSingleElements = singleElements.filter(element => !elementExists(text, element));
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,9 @@
|
||||||
{
|
{
|
||||||
"include": "#strings"
|
"include": "#strings"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"include": "#script-blocks"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"include": "#punctuation"
|
"include": "#punctuation"
|
||||||
}
|
}
|
||||||
|
@ -64,6 +67,24 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"script-blocks": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"begin": "@(client|server)\\s*\\{",
|
||||||
|
"end": "\\}",
|
||||||
|
"beginCaptures": {
|
||||||
|
"0": { "name": "keyword.control.blueprint" },
|
||||||
|
"1": { "name": "entity.name.function.blueprint" }
|
||||||
|
},
|
||||||
|
"contentName": "source.js.embedded.blueprint",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "source.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
|
@ -71,7 +92,7 @@
|
||||||
"name": "support.type.property-name.blueprint"
|
"name": "support.type.property-name.blueprint"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"match": "(?<!:)(src|type|href|\\w+)\\s*:",
|
"match": "(?<!:)(src|type|href|id|\\w+)\\s*:",
|
||||||
"captures": {
|
"captures": {
|
||||||
"1": { "name": "support.type.property-name.blueprint" }
|
"1": { "name": "support.type.property-name.blueprint" }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (token.type === "identifier") {
|
||||||
if (this.options.debug) {
|
if (this.options.debug) {
|
||||||
console.log(
|
console.log(
|
||||||
|
|
|
@ -46,6 +46,20 @@ class BlueprintBuilder {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.fileHandler.writeCompiledFiles(outputDir, baseName, result.html, result.css);
|
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) {
|
if (this.options.debug) {
|
||||||
console.log("[DEBUG] Build completed successfully");
|
console.log("[DEBUG] Build completed successfully");
|
||||||
}
|
}
|
||||||
|
@ -54,6 +68,7 @@ class BlueprintBuilder {
|
||||||
return {
|
return {
|
||||||
success: result.success,
|
success: result.success,
|
||||||
errors: result.errors,
|
errors: result.errors,
|
||||||
|
hasServerCode: result.hasServerCode
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.options.debug) {
|
if (this.options.debug) {
|
||||||
|
@ -61,6 +76,7 @@ class BlueprintBuilder {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
hasServerCode: false,
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
|
|
@ -51,18 +51,25 @@ class BlueprintCompiler {
|
||||||
|
|
||||||
const html = this.htmlGenerator.generateHTML(ast);
|
const html = this.htmlGenerator.generateHTML(ast);
|
||||||
const css = this.cssGenerator.generateCSS();
|
const css = this.cssGenerator.generateCSS();
|
||||||
|
const hasServerCode = this.htmlGenerator.hasServerCode();
|
||||||
|
const serverCode = hasServerCode ? this.htmlGenerator.generateServerCode() : '';
|
||||||
|
|
||||||
const headContent = this.metadataManager.generateHeadContent(baseName);
|
const headContent = this.metadataManager.generateHeadContent(baseName);
|
||||||
const finalHtml = this.htmlGenerator.generateFinalHtml(headContent, html);
|
const finalHtml = this.htmlGenerator.generateFinalHtml(headContent, html);
|
||||||
|
|
||||||
if (this.options.debug) {
|
if (this.options.debug) {
|
||||||
console.log("[DEBUG] Compilation completed successfully");
|
console.log("[DEBUG] Compilation completed successfully");
|
||||||
|
if (hasServerCode) {
|
||||||
|
console.log("[DEBUG] Server code generated");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
html: finalHtml,
|
html: finalHtml,
|
||||||
css: css,
|
css: css,
|
||||||
|
hasServerCode: hasServerCode,
|
||||||
|
serverCode: serverCode,
|
||||||
errors: [],
|
errors: [],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -73,6 +80,8 @@ class BlueprintCompiler {
|
||||||
success: false,
|
success: false,
|
||||||
html: null,
|
html: null,
|
||||||
css: null,
|
css: null,
|
||||||
|
hasServerCode: false,
|
||||||
|
serverCode: null,
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
|
|
@ -6,6 +6,8 @@ const StandardElementGenerator = require("./generators/StandardElementGenerator"
|
||||||
const RootNodeGenerator = require("./generators/RootNodeGenerator");
|
const RootNodeGenerator = require("./generators/RootNodeGenerator");
|
||||||
const InputElementGenerator = require("./generators/InputElementGenerator");
|
const InputElementGenerator = require("./generators/InputElementGenerator");
|
||||||
const MediaElementGenerator = require("./generators/MediaElementGenerator");
|
const MediaElementGenerator = require("./generators/MediaElementGenerator");
|
||||||
|
const JavaScriptGenerator = require("./generators/JavaScriptGenerator");
|
||||||
|
const ServerCodeGenerator = require("./generators/ServerCodeGenerator");
|
||||||
const HTMLTemplate = require("./templates/HTMLTemplate");
|
const HTMLTemplate = require("./templates/HTMLTemplate");
|
||||||
const StringUtils = require("./utils/StringUtils");
|
const StringUtils = require("./utils/StringUtils");
|
||||||
|
|
||||||
|
@ -21,6 +23,9 @@ class HTMLGenerator {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.cssGenerator = cssGenerator;
|
this.cssGenerator = cssGenerator;
|
||||||
this.htmlTemplate = new HTMLTemplate(options);
|
this.htmlTemplate = new HTMLTemplate(options);
|
||||||
|
this.serverGenerator = new ServerCodeGenerator(options);
|
||||||
|
this.jsGenerator = new JavaScriptGenerator(options, this.serverGenerator);
|
||||||
|
this.currentElement = null;
|
||||||
|
|
||||||
|
|
||||||
this.generators = [
|
this.generators = [
|
||||||
|
@ -86,6 +91,10 @@ class HTMLGenerator {
|
||||||
console.log("[HTMLGenerator] Node details:", StringUtils.safeStringify(node));
|
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 (node.type === "element" && node.tag === "page") {
|
||||||
if (this.options.debug) {
|
if (this.options.debug) {
|
||||||
|
@ -94,20 +103,120 @@ class HTMLGenerator {
|
||||||
return "";
|
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) {
|
for (const generator of this.generators) {
|
||||||
if (generator.canHandle(node)) {
|
if (generator.canHandle(node)) {
|
||||||
if (this.options.debug) {
|
if (this.options.debug) {
|
||||||
console.log(`[HTMLGenerator] Using ${generator.constructor.name} for node`);
|
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) {
|
if (this.options.debug) {
|
||||||
console.log(`[HTMLGenerator] No generator found for node type: ${node.type}`);
|
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 "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +286,28 @@ class HTMLGenerator {
|
||||||
* @returns {string} - A complete HTML document containing the provided head and body content.
|
* @returns {string} - A complete HTML document containing the provided head and body content.
|
||||||
*/
|
*/
|
||||||
generateFinalHtml(headContent, bodyContent) {
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,142 @@ class TokenParser {
|
||||||
continue;
|
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)) {
|
if (/[a-zA-Z]/.test(char)) {
|
||||||
let value = "";
|
let value = "";
|
||||||
const startColumn = column;
|
const startColumn = column;
|
||||||
|
|
|
@ -49,6 +49,22 @@ class ButtonElementGenerator {
|
||||||
|
|
||||||
let attributes = "";
|
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") {
|
if (node.parent?.tag === "link") {
|
||||||
const linkInfo = this.linkProcessor.processLink(node.parent);
|
const linkInfo = this.linkProcessor.processLink(node.parent);
|
||||||
attributes += this.linkProcessor.getButtonClickHandler(linkInfo);
|
attributes += this.linkProcessor.getButtonClickHandler(linkInfo);
|
||||||
|
|
|
@ -54,6 +54,22 @@ class StandardElementGenerator {
|
||||||
|
|
||||||
let attributes = "";
|
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-"))) {
|
if (node.props.find((p) => typeof p === "string" && p.startsWith("data-"))) {
|
||||||
const dataProps = node.props.filter(
|
const dataProps = node.props.filter(
|
||||||
(p) => typeof p === "string" && p.startsWith("data-")
|
(p) => typeof p === "string" && p.startsWith("data-")
|
||||||
|
|
|
@ -538,7 +538,7 @@ const ELEMENT_MAPPINGS = {
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
tag: "div",
|
tag: "div",
|
||||||
defaultProps: ["raised", "card"],
|
defaultProps: ["card"],
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
tag: "div",
|
tag: "div",
|
||||||
|
|
285
lib/server.js
285
lib/server.js
|
@ -11,6 +11,7 @@ class BlueprintServer {
|
||||||
this.wsInstance = expressWs(this.app);
|
this.wsInstance = expressWs(this.app);
|
||||||
this.options = {
|
this.options = {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
apiPort: 3001,
|
||||||
srcDir: "./src",
|
srcDir: "./src",
|
||||||
outDir: "./dist",
|
outDir: "./dist",
|
||||||
liveReload: false,
|
liveReload: false,
|
||||||
|
@ -19,6 +20,8 @@ class BlueprintServer {
|
||||||
};
|
};
|
||||||
this.clients = new Map();
|
this.clients = new Map();
|
||||||
this.filesWithErrors = new Set();
|
this.filesWithErrors = new Set();
|
||||||
|
this.apiServers = new Map();
|
||||||
|
this.apiPorts = new Map();
|
||||||
this.setupServer();
|
this.setupServer();
|
||||||
if (this.options.liveReload) {
|
if (this.options.liveReload) {
|
||||||
const watcher = chokidar.watch([], {
|
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() {
|
async buildAll() {
|
||||||
this.log("INFO", "Building all Blueprint files...", "lightGray");
|
this.log("INFO", "Building all Blueprint files...", "lightGray");
|
||||||
if (fs.existsSync(this.options.outDir)) {
|
if (fs.existsSync(this.options.outDir)) {
|
||||||
|
@ -70,8 +94,17 @@ class BlueprintServer {
|
||||||
);
|
);
|
||||||
this.ensureDirectoryExistence(outputPath);
|
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));
|
const result = builder.build(file, path.dirname(outputPath));
|
||||||
|
|
||||||
|
if (result.success && result.hasServerCode) {
|
||||||
|
const fileName = relativePath.replace(/\.bp$/, "");
|
||||||
|
this.startApiServer(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
success = false;
|
success = false;
|
||||||
errors.push({ file, errors: result.errors });
|
errors.push({ file, errors: result.errors });
|
||||||
|
@ -138,8 +171,19 @@ class BlueprintServer {
|
||||||
fs.readFile(htmlPath, "utf8", (err, data) => {
|
fs.readFile(htmlPath, "utf8", (err, data) => {
|
||||||
if (err) return next();
|
if (err) return next();
|
||||||
let html = data;
|
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 = `
|
const script = `
|
||||||
<script>
|
<script>
|
||||||
|
window.blueprintServerPort = ${apiPort};
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
let currentPage = window.location.pathname.replace(/^\\//, '') || 'index.html';
|
let currentPage = window.location.pathname.replace(/^\\//, '') || 'index.html';
|
||||||
console.log('Current page:', currentPage);
|
console.log('Current page:', currentPage);
|
||||||
|
@ -240,7 +284,33 @@ class BlueprintServer {
|
||||||
|
|
||||||
const htmlPath = path.join(this.options.outDir, req.path + ".html");
|
const htmlPath = path.join(this.options.outDir, req.path + ".html");
|
||||||
if (fs.existsSync(htmlPath)) {
|
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 === "/") {
|
} else if (req.path === "/") {
|
||||||
const pages = fs
|
const pages = fs
|
||||||
.readdirSync(this.options.outDir)
|
.readdirSync(this.options.outDir)
|
||||||
|
@ -311,83 +381,35 @@ class BlueprintServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupWatcher(watcher) {
|
setupWatcher(watcher) {
|
||||||
watcher.on("change", async (filepath) => {
|
watcher.on("change", async (filePath) => {
|
||||||
if (filepath.endsWith(".bp")) {
|
if (!filePath.endsWith(".bp")) return;
|
||||||
this.log("INFO", `File ${filepath} has been changed`, "blue");
|
this.log("INFO", `File changed: ${filePath}`, "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) {
|
const result = await this.buildFile(filePath);
|
||||||
this.log("SUCCESS", "Rebuilt successfully", "green");
|
|
||||||
|
|
||||||
this.filesWithErrors.delete(filepath);
|
if (result.success) {
|
||||||
|
this.log("SUCCESS", `Rebuilt ${filePath}`, "green");
|
||||||
|
this.filesWithErrors.delete(filePath);
|
||||||
|
|
||||||
const htmlFile = relativePath.replace(/\.bp$/, ".html");
|
if (result.hasServerCode) {
|
||||||
const htmlPath = path.join(this.options.outDir, htmlFile);
|
const relativePath = path.relative(this.options.srcDir, filePath);
|
||||||
|
const fileName = relativePath.replace(/\.bp$/, "");
|
||||||
try {
|
this.startApiServer(fileName);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
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 = {
|
module.exports = {
|
||||||
escapeHTML,
|
escapeHTML,
|
||||||
toKebabCase,
|
toKebabCase,
|
||||||
safeStringify
|
safeStringify,
|
||||||
|
generateRandomId
|
||||||
};
|
};
|
|
@ -10,6 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-ws": "^5.0.2",
|
"express-ws": "^5.0.2",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue