feat: code blocks beta
This commit is contained in:
parent
362b7aa15e
commit
1ecb6d8682
19 changed files with 756 additions and 94 deletions
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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue