@liascript/devserver
Version:
Run a development server for LiaScript locally
360 lines (359 loc) • 19.3 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.init = init;
exports.start = start;
exports.stop = stop;
exports.gotoLine = gotoLine;
var express = require("express");
var fs = require("fs");
var path = require("path");
var cors = require('cors');
var handlebars = require('express-handlebars');
var ip = require('ip');
var open = require('open');
var bodyParser = require('body-parser');
var reload = require('reload');
var chokidar = require('chokidar');
var app = express();
app.use(bodyParser.json());
var dirname = '';
var node_modules;
var liascriptPath = '';
var reloadInstance = null;
var watcher = null;
var clients = [];
var gotoScript = "<script>\nif (!window.LIA) {\n window.LIA = {}\n}\n\nvar filename__ = document.location.search.replace(\"?\"+document.location.origin, \"\")\n\nwindow.LIA.lineGoto = function(linenumber) {\n fetch(\"/lineGoto\", {\n method: \"POST\",\n headers: {'Content-Type': 'application/json'}, \n body: JSON.stringify({\n \"linenumber\": linenumber,\n \"filename\": filename__\n })\n }).then(res => {\n console.log(\"Goto line\", linenumber);\n });\n}\n\nconst events = new EventSource('/gotoLine');\nevents.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data);\n if (data.filename == filename__) {\n console.log(\"goto line:\", data.linenumber);\n window.LIA.gotoLine(data.linenumber)\n }\n } catch (e) {\n console.warn(\"gotoLine failed\")\n }\n};\n</script>";
var serverPointer;
init(__dirname);
function init(serverPath, nodeModulesPath) {
dirname = serverPath || path.join(__dirname, '..');
node_modules = nodeModulesPath || path.join(dirname, 'node_modules');
liascriptPath = path.resolve(path.join(node_modules, '@liascript/editor/dist'));
}
function start(port, hostname, input, responsiveVoice, liveReload, openInBrowser, testOnline, gotoCallback) {
return __awaiter(this, void 0, void 0, function () {
var project, stats, watchPath, err_1, localURL, server;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
port = port || 3000;
hostname = hostname || 'localhost';
openInBrowser = openInBrowser || false;
input = input || '.';
liveReload = liveReload || false;
testOnline = testOnline || false;
project = {
path: input,
readme: undefined,
};
if (input) {
stats = fs.lstatSync(input);
// Is it a directory?
if (stats.isDirectory()) {
project.path = input;
}
else if (stats.isFile()) {
project.path = path.dirname(input);
project.readme = path.basename(input);
}
}
app.set('view engine', 'hbs');
app.engine('hbs', handlebars({
layoutsDir: path.resolve(path.join(dirname, 'views/layouts')),
defaultLayout: 'main',
extname: 'hbs',
}));
app.set('views', path.resolve(path.join(dirname, 'views')));
if (!liveReload) return [3 /*break*/, 4];
watchPath = path.join(project.path, project.readme || '');
console.log("\u2728 watching for changes on: \"".concat(watchPath, "\""));
_a.label = 1;
case 1:
_a.trys.push([1, 3, , 4]);
return [4 /*yield*/, reload(app)
// Watch the file for changes using chokidar (more reliable than fs.watch)
];
case 2:
reloadInstance = _a.sent();
// Watch the file for changes using chokidar (more reliable than fs.watch)
watcher = chokidar.watch(watchPath, {
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 50,
},
});
watcher.on('change', function (filePath) {
console.log('📝 file changed, reloading...');
reloadInstance.reload();
});
watcher.on('error', function (error) {
console.error('Watcher error:', error);
});
return [3 /*break*/, 4];
case 3:
err_1 = _a.sent();
console.error('Reload could not start:', err_1);
return [3 /*break*/, 4];
case 4:
app.get('/', function (req, res) {
res.redirect('/home');
});
app.get('/gotoLine', eventsHandler);
app.get('/home*', function (req, res) {
var currentPath = project.path + '/' + req.params[0];
var stats = fs.lstatSync(currentPath);
// Is it a directory?
if (stats.isDirectory()) {
var files = fs.readdirSync(currentPath).filter(function (e) {
return e[0] !== '.';
});
var basePath = '/home';
var pathNames = req.params[0].split('/').filter(function (e) {
return e !== '';
});
var paths = [];
for (var i = 0; i < pathNames.length; i++) {
basePath += '/' + pathNames[i];
paths.push({ name: pathNames[i], href: basePath });
}
res.render('main', {
layout: 'index',
path: paths,
file: files
.map(function (file) {
return {
name: file,
href: "http://".concat(hostname, ":").concat(port, "/home").concat(req.params[0], "/").concat(file),
isDirectory: fs.lstatSync(currentPath + '/' + file).isDirectory(),
};
})
.sort(function (a, b) {
if (a.isDirectory && !b.isDirectory) {
return -1;
}
else if (!a.isDirectory && b.isDirectory) {
return 1;
}
else {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) {
return -1;
}
else {
return 1;
}
}
return 0;
}),
});
}
else if (stats.isFile()) {
if (req.params[0].toLocaleLowerCase().endsWith('.md')) {
if (testOnline) {
res.redirect("https://LiaScript.github.io/course/?http://".concat(hostname, ":").concat(port, "/").concat(req.params[0]));
}
else {
res.redirect("/liascript/index.html?http://".concat(hostname, ":").concat(port, "/").concat(req.params[0]));
}
}
else {
res.sendFile(req.params[0], { root: project.path });
}
}
else {
res.send('ups, something went wrong');
}
});
app.get('/liascript/', function (req, res) {
res.redirect('/liascript/index.html');
});
app.get('/liascript/index.html', function (req, res) {
// ------------------------------------
if (liveReload && responsiveVoice) {
fs.readFile(liascriptPath + '/index.html', 'utf8', function (err, data) {
if (err || !data) {
res.status(500).send('index.html not found or could not be read');
return;
}
res.send(data.replace('</head>', "<script type='text/javascript' src='/reload/reload.js'></script>\n <script type='text/javascript' src='https://code.responsivevoice.org/responsivevoice.js?key=".concat(responsiveVoice, "'></script>\n ").concat(gotoScript, "\n </head>")));
});
}
// ------------------------------------
else if (liveReload) {
fs.readFile(liascriptPath + '/index.html', 'utf8', function (err, data) {
if (err || !data) {
res.status(500).send('index.html not found or could not be read');
return;
}
res.send(data.replace('</head>', "<script type='text/javascript' src='/reload/reload.js'></script>\n ".concat(gotoScript, "\n </head>")));
});
}
// ------------------------------------
else if (responsiveVoice) {
fs.readFile(liascriptPath + '/index.html', 'utf8', function (err, data) {
if (err || !data) {
res.status(500).send('index.html not found or could not be read');
return;
}
res.send(data.replace('</head>', "<script type='text/javascript' src='https://code.responsivevoice.org/responsivevoice.js?key=".concat(responsiveVoice, "'></script>\n ").concat(gotoScript, "\n </head>")));
});
}
// ------------------------------------
else {
fs.readFile(liascriptPath + '/index.html', 'utf8', function (err, data) {
if (err || !data) {
res.status(500).send('index.html not found or could not be read');
return;
}
res.send(data.replace('</head>', "".concat(gotoScript, "</head>")));
});
}
});
// load everything from the liascript folder
app.get('/liascript/*', function (req, res) {
res.sendFile(req.params[0], { root: liascriptPath }, function (err) {
if (err) {
// Extract the file path by removing the '/liascript/' prefix
var projectPath = req.params[0];
console.log("File not found in liascriptPath, trying project.path: ".concat(projectPath), project.path);
res.sendFile(projectPath, { root: project.path });
}
});
});
// ignore this one
app.get('/sw.js', function (req, res) { });
app.get('/favicon.ico', function (req, res) { });
// react to click-events
app.post('/lineGoto', function (req, res) {
if (gotoCallback) {
try {
var linenumber = req.body.linenumber;
var filename = req.body.filename;
gotoCallback(linenumber, filename);
}
catch (e) {
console.warn("lineGoto event with wrong datatype, you have to provide {'linenumber': int, 'filename': string}");
}
}
return res.json({});
});
// everything else comes from the current project folder
app.use('/*', cors(), function (req, res, next) {
// Skip reload and socket.io paths - let reload package handle them
if (req.originalUrl.startsWith('/reload/') ||
req.originalUrl.startsWith('/socket.io')) {
return next();
}
res.sendFile(req.originalUrl, { root: project.path });
});
localURL = 'http://' + hostname + ':' + port;
if (project.path && project.readme) {
localURL +=
'/liascript/index.html?http://' +
hostname +
':' +
port +
'/' +
project.readme;
}
if (testOnline && project.readme) {
localURL =
'https://LiaScript.github.io/course/?http://' +
hostname +
':' +
port +
'/' +
project.readme;
}
server = app.listen(port);
server.on('error', function (e) {
throw e;
});
if (openInBrowser) {
open(localURL);
}
console.log('📡 starting server');
console.log(" - local: ".concat(localURL));
console.log(" - on your network: ".concat(localURL.replace(hostname, ip.address())));
serverPointer = server;
return [2 /*return*/];
}
});
});
}
function stop() {
if (serverPointer) {
serverPointer.close();
}
if (watcher) {
watcher.close();
watcher = null;
}
if (reloadInstance) {
reloadInstance = null;
}
}
function gotoLine(linenumber, filename) {
clients.forEach(function (client) {
return client.response.write("data: ".concat(JSON.stringify({
linenumber: linenumber,
filename: filename,
}), "\n\n"));
});
}
function eventsHandler(request, response, next) {
var headers = {
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
};
response.writeHead(200, headers);
var data = "data: \n\n";
response.write(data);
var clientId = Date.now();
var newClient = {
id: clientId,
response: response,
};
clients.push(newClient);
request.on('close', function () {
//console.log(`${clientId} Connection closed`)
clients = clients.filter(function (client) { return client.id !== clientId; });
});
}