pxt-core
Version:
Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors
1,277 lines • 51.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.serveAsync = exports.compileScriptAsync = exports.expandDocFileTemplate = exports.expandDocTemplateCore = exports.expandHtml = exports.lookupDocFile = void 0;
const fs = require("fs");
const path = require("path");
const http = require("http");
const https = require("https");
const url = require("url");
const querystring = require("querystring");
const nodeutil = require("./nodeutil");
const hid = require("./hid");
const net = require("net");
const storage = require("./storage");
const subwebapp_1 = require("./subwebapp");
const util_1 = require("util");
var U = pxt.Util;
var Cloud = pxt.Cloud;
const userProjectsDirName = "projects";
let root = "";
let dirs = [""];
let docfilesdirs = [""];
let userProjectsDir = path.join(process.cwd(), userProjectsDirName);
let docsDir = "";
let packagedDir = "";
let localHexCacheDir = path.join("built", "hexcache");
let serveOptions;
function setupDocfilesdirs() {
docfilesdirs = [
"docfiles",
path.join(nodeutil.pxtCoreDir, "docfiles")
];
}
function setupRootDir() {
root = nodeutil.targetDir;
console.log("Starting server in", root);
console.log(`With pxt core at ${nodeutil.pxtCoreDir}`);
dirs = [
"built/web",
path.join(nodeutil.targetDir, "docs"),
path.join(nodeutil.targetDir, "built"),
path.join(nodeutil.targetDir, "sim/public"),
path.join(nodeutil.targetDir, "node_modules", `pxt-${pxt.appTarget.id}-sim`, "public"),
path.join(nodeutil.pxtCoreDir, "built/web"),
path.join(nodeutil.pxtCoreDir, "webapp/public"),
path.join(nodeutil.pxtCoreDir, "common-docs"),
path.join(nodeutil.pxtCoreDir, "docs"),
];
docsDir = path.join(root, "docs");
packagedDir = path.join(root, "built/packaged");
setupDocfilesdirs();
setupProjectsDir();
pxt.debug(`docs dir:\r\n ${docsDir}`);
pxt.debug(`doc files dir: \r\n ${docfilesdirs.join("\r\n ")}`);
pxt.debug(`dirs:\r\n ${dirs.join('\r\n ')}`);
pxt.debug(`projects dir: ${userProjectsDir}`);
}
function setupProjectsDir() {
nodeutil.mkdirP(userProjectsDir);
}
const statAsync = (0, util_1.promisify)(fs.stat);
const readdirAsync = (0, util_1.promisify)(fs.readdir);
const readFileAsync = (0, util_1.promisify)(fs.readFile);
const writeFileAsync = (0, util_1.promisify)(fs.writeFile);
const unlinkAsync = (0, util_1.promisify)(fs.unlink);
function existsAsync(fn) {
return new Promise((resolve, reject) => {
fs.exists(fn, resolve);
});
}
function statOptAsync(fn) {
return statAsync(fn)
.then(st => st, err => null);
}
function throwError(code, msg = null) {
let err = new Error(msg || "Error " + code);
err.statusCode = code;
throw err;
}
function readAssetsAsync(logicalDirname) {
let dirname = path.join(userProjectsDir, logicalDirname, "assets");
let pref = "http://" + serveOptions.hostname + ":" + serveOptions.port + "/assets/" + logicalDirname + "/";
return readdirAsync(dirname)
.catch(err => [])
.then(res => U.promiseMapAll(res, fn => statAsync(path.join(dirname, fn)).then(res => ({
name: fn,
size: res.size,
url: pref + fn
}))))
.then(res => ({
files: res
}));
}
const HEADER_JSON = ".header.json";
async function readPkgAsync(logicalDirname, fileContents = false) {
let dirname = path.join(userProjectsDir, logicalDirname);
let buf = await readFileAsync(path.join(dirname, pxt.CONFIG_NAME));
let cfg = JSON.parse(buf.toString("utf8"));
let r = {
path: logicalDirname,
config: cfg,
header: null,
files: []
};
for (let fn of pxt.allPkgFiles(cfg).concat([pxt.github.GIT_JSON, pxt.SIMSTATE_JSON])) {
let st = await statOptAsync(path.join(dirname, fn));
let ff = {
name: fn,
mtime: st ? st.mtime.getTime() : null
};
let thisFileContents = st && fileContents;
if (!st && fn == pxt.SIMSTATE_JSON)
continue;
if (fn == pxt.github.GIT_JSON) {
// skip .git.json altogether if missing
if (!st)
continue;
thisFileContents = true;
}
if (thisFileContents) {
let buf = await readFileAsync(path.join(dirname, fn));
ff.content = buf.toString("utf8");
}
r.files.push(ff);
}
if (await existsAsync(path.join(dirname, "icon.jpeg"))) {
r.icon = "/icon/" + logicalDirname;
}
// now try reading the header
buf = await readFileAsync(path.join(dirname, HEADER_JSON))
.then(b => b, err => null);
if (buf && buf.length)
r.header = JSON.parse(buf.toString("utf8"));
return r;
}
function writeScreenshotAsync(logicalDirname, screenshotUri, iconUri) {
console.log('writing screenshot...');
const dirname = path.join(userProjectsDir, logicalDirname);
nodeutil.mkdirP(dirname);
function writeUriAsync(name, uri) {
if (!uri)
return Promise.resolve();
const m = uri.match(/^data:image\/(png|jpeg);base64,(.*)$/);
if (!m)
return Promise.resolve();
const ext = m[1];
const data = m[2];
const fn = path.join(dirname, name + "." + ext);
console.log(`writing ${fn}`);
return writeFileAsync(fn, Buffer.from(data, 'base64'));
}
return Promise.all([
writeUriAsync("screenshot", screenshotUri),
writeUriAsync("icon", iconUri)
]).then(() => { });
}
function writePkgAssetAsync(logicalDirname, data) {
const dirname = path.join(userProjectsDir, logicalDirname, "assets");
nodeutil.mkdirP(dirname);
return writeFileAsync(dirname + "/" + data.name, Buffer.from(data.data, data.encoding || "base64"))
.then(() => ({
name: data.name
}));
}
function writePkgAsync(logicalDirname, data) {
const dirname = path.join(userProjectsDir, logicalDirname);
nodeutil.mkdirP(dirname);
return U.promiseMapAll(data.files, f => readFileAsync(path.join(dirname, f.name))
.then(buf => {
if (f.name == pxt.CONFIG_NAME) {
try {
if (!pxt.Package.parseAndValidConfig(f.content)) {
pxt.log("Trying to save invalid JSON config");
pxt.debug(f.content);
throwError(410);
}
}
catch (e) {
pxt.log("Trying to save invalid format JSON config");
pxt.log(e);
pxt.debug(f.content);
throwError(410);
}
}
if (buf.toString("utf8") !== f.prevContent) {
pxt.log(`merge error for ${f.name}: previous content changed...`);
throwError(409);
}
}, err => { }))
// no conflict, proceed with writing
.then(() => U.promiseMapAll(data.files, f => {
let d = f.name.replace(/\/[^\/]*$/, "");
if (d != f.name)
nodeutil.mkdirP(path.join(dirname, d));
const fn = path.join(dirname, f.name);
return f.content == null ? unlinkAsync(fn) : writeFileAsync(fn, f.content);
}))
.then(() => {
if (data.header)
return writeFileAsync(path.join(dirname, HEADER_JSON), JSON.stringify(data.header, null, 4));
})
.then(() => readPkgAsync(logicalDirname, false));
}
function returnDirAsync(logicalDirname, depth) {
logicalDirname = logicalDirname.replace(/^\//, "");
const dirname = path.join(userProjectsDir, logicalDirname);
// load packages under /projects, 3 level deep
return existsAsync(path.join(dirname, pxt.CONFIG_NAME))
// read package if pxt.json exists
.then(ispkg => Promise.all([
// current folder
ispkg ? readPkgAsync(logicalDirname).then(r => [r], err => undefined) : Promise.resolve(undefined),
// nested packets
depth <= 1 ? Promise.resolve(undefined)
: readdirAsync(dirname).then(files => U.promiseMapAll(files, fn => statAsync(path.join(dirname, fn)).then(st => {
if (fn[0] != "." && st.isDirectory())
return returnDirAsync(logicalDirname + "/" + fn, depth - 1);
else
return undefined;
})).then(U.concat))
]))
// drop empty arrays
.then(rs => rs.filter(r => !!r))
.then(U.concat);
}
function isAuthorizedLocalRequest(req) {
// validate token
return req.headers["authorization"]
&& req.headers["authorization"] == serveOptions.localToken;
}
function getCachedHexAsync(sha) {
if (!sha) {
return Promise.resolve();
}
let hexFile = path.resolve(localHexCacheDir, sha + ".hex");
return existsAsync(hexFile)
.then((results) => {
if (!results) {
console.log(`offline HEX not found: ${hexFile}`);
return Promise.resolve(null);
}
console.log(`serving HEX from offline cache: ${hexFile}`);
return readFileAsync(hexFile)
.then((fileContent) => {
return {
enums: [],
functions: [],
hex: fileContent.toString()
};
});
});
}
async function handleApiStoreRequestAsync(req, res, elts) {
const meth = req.method.toUpperCase();
const container = decodeURIComponent(elts[0]);
const key = decodeURIComponent(elts[1]);
if (!container || !key) {
throw throwError(400, "malformed api/store request: " + req.url);
}
const origin = req.headers['origin'] || '*';
res.setHeader('Access-Control-Allow-Origin', origin);
if (meth === "GET") {
const val = await storage.getAsync(container, key);
if (val) {
if (typeof val === "object") {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf8' });
res.end(JSON.stringify(val));
}
else {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf8' });
res.end(val.toString());
}
}
else {
res.writeHead(204);
res.end();
}
}
else if (meth === "POST") {
const srec = (await nodeutil.readResAsync(req)).toString("utf8");
const rec = JSON.parse(srec);
await storage.setAsync(container, key, rec);
res.writeHead(200);
res.end();
}
else if (meth === "DELETE") {
await storage.delAsync(container, key);
res.writeHead(200);
res.end();
}
else if (meth === "OPTIONS") {
const allowedHeaders = req.headers['access-control-request-headers'] || 'Content-Type';
const allowedMethods = req.headers['access-control-request-method'] || 'GET, POST, DELETE';
res.setHeader('Access-Control-Allow-Headers', allowedHeaders);
res.setHeader('Access-Control-Allow-Methods', allowedMethods);
res.writeHead(200);
res.end();
}
else {
throw res.writeHead(400, "Unsupported HTTP method: " + meth);
}
}
function handleApiAsync(req, res, elts) {
const opts = querystring.parse(url.parse(req.url).query);
const innerPath = elts.slice(2).join("/").replace(/^\//, "");
const filename = path.resolve(path.join(userProjectsDir, innerPath));
const meth = req.method.toUpperCase();
const cmd = meth + " " + elts[1];
const readJsonAsync = () => nodeutil.readResAsync(req)
.then(buf => JSON.parse(buf.toString("utf8")));
if (cmd == "GET list")
return returnDirAsync(innerPath, 3)
.then(lst => {
return {
pkgs: lst
};
});
else if (cmd == "GET stat")
return statOptAsync(filename)
.then(st => {
if (!st)
return {};
else
return {
mtime: st.mtime.getTime()
};
});
else if (cmd == "GET pkg")
return readPkgAsync(innerPath, true);
else if (cmd == "POST pkg")
return readJsonAsync()
.then(d => writePkgAsync(innerPath, d));
else if (cmd == "POST pkgasset")
return readJsonAsync()
.then(d => writePkgAssetAsync(innerPath, d));
else if (cmd == "GET pkgasset")
return readAssetsAsync(innerPath);
else if (cmd == "POST deploy" && pxt.commands.hasDeployFn())
return readJsonAsync()
.then(pxt.commands.deployAsync)
.then((boardCount) => {
return {
boardCount: boardCount
};
});
else if (cmd == "POST screenshot")
return readJsonAsync()
.then(d => writeScreenshotAsync(innerPath, d.screenshot, d.icon));
else if (cmd == "GET compile")
return getCachedHexAsync(innerPath)
.then((res) => {
if (!res) {
return {
notInOfflineCache: true
};
}
return res;
});
else if (cmd == "GET md" && pxt.appTarget.id + "/" == innerPath.slice(0, pxt.appTarget.id.length + 1)) {
// innerpath start with targetid
return readMdAsync(innerPath.slice(pxt.appTarget.id.length + 1), opts["lang"]);
}
else if (cmd == "GET config" && new RegExp(`${pxt.appTarget.id}\/targetconfig(\/v[0-9.]+)?$`).test(innerPath)) {
// target config
return readFileAsync("targetconfig.json").then(buf => JSON.parse(buf.toString("utf8")));
}
else
throw throwError(400, `unknown command ${cmd.slice(0, 140)}`);
}
function lookupDocFile(name) {
if (docfilesdirs.length <= 1)
setupDocfilesdirs();
for (let d of docfilesdirs) {
let foundAt = path.join(d, name);
if (fs.existsSync(foundAt))
return foundAt;
}
return null;
}
exports.lookupDocFile = lookupDocFile;
function expandHtml(html, params, appTheme) {
let theme = U.flatClone(appTheme || pxt.appTarget.appTheme);
html = expandDocTemplateCore(html);
params = params || {};
params["name"] = params["name"] || pxt.appTarget.appTheme.title;
params["description"] = params["description"] || pxt.appTarget.appTheme.description;
params["locale"] = params["locale"] || pxt.appTarget.appTheme.defaultLocale || "en";
// page overrides
let m = /<title>([^<>@]*)<\/title>/.exec(html);
if (m)
params["name"] = m[1];
m = /<meta name="Description" content="([^"@]*)"/i.exec(html);
if (m)
params["description"] = m[1];
let d = {
html: html,
params: params,
theme: theme,
// Note that breadcrumb and filepath expansion are not supported in the cloud
// so we don't do them here either.
};
pxt.docs.prepTemplate(d);
return d.finish().replace(/@-(\w+)-@/g, (f, w) => "@" + w + "@");
}
exports.expandHtml = expandHtml;
function expandDocTemplateCore(template) {
template = template
.replace(/<!--\s*@include\s+(\S+)\s*-->/g, (full, fn) => {
return `
<!-- include ${fn} -->
${expandDocFileTemplate(fn)}
<!-- end include ${fn} -->
`;
});
return template;
}
exports.expandDocTemplateCore = expandDocTemplateCore;
function expandDocFileTemplate(name) {
let fn = lookupDocFile(name);
let template = fn ? fs.readFileSync(fn, "utf8") : "";
return expandDocTemplateCore(template);
}
exports.expandDocFileTemplate = expandDocFileTemplate;
let wsSerialClients = [];
let webappReady = false;
function initSocketServer(wsPort, hostname) {
console.log(`starting local ws server at ${wsPort}...`);
const WebSocket = require('faye-websocket');
function startSerial(request, socket, body) {
let ws = new WebSocket(request, socket, body);
wsSerialClients.push(ws);
ws.on('message', function (event) {
// ignore
});
ws.on('close', function (event) {
console.log('ws connection closed');
wsSerialClients.splice(wsSerialClients.indexOf(ws), 1);
ws = null;
});
ws.on('error', function () {
console.log('ws connection closed');
wsSerialClients.splice(wsSerialClients.indexOf(ws), 1);
ws = null;
});
}
function objToString(obj) {
if (obj == null)
return "null";
let r = "{\n";
for (let k of Object.keys(obj)) {
r += " " + k + ": ";
let s = JSON.stringify(obj[k]);
if (!s)
s = "(null)";
if (s.length > 60)
s = s.slice(0, 60) + "...";
r += s + "\n";
}
r += "}";
return r;
}
let hios = {};
function startHID(request, socket, body) {
let ws = new WebSocket(request, socket, body);
ws.on('open', () => {
ws.send(JSON.stringify({ id: "ready" }));
});
ws.on('message', function (event) {
try {
let msg = JSON.parse(event.data);
pxt.debug(`hid: msg ${msg.op}`); // , objToString(msg.arg))
// check that HID is installed
if (!hid.isInstalled(true)) {
if (!ws)
return;
ws.send(JSON.stringify({
result: {
errorMessage: "node-hid not installed",
},
op: msg.op,
id: msg.id
}));
return;
}
Promise.resolve()
.then(() => {
let hio = hios[msg.arg.path];
if (!hio && msg.arg.path)
hios[msg.arg.path] = hio = hid.hf2ConnectAsync(msg.arg.path, !!msg.arg.raw);
return hio;
})
.then(hio => {
switch (msg.op) {
case "disconnect":
return hio.disconnectAsync()
.then(() => ({}));
case "init":
return hio.reconnectAsync()
.then(() => {
hio.io.onEvent = v => {
if (!ws)
return;
ws.send(JSON.stringify({
op: "event",
result: {
path: msg.arg.path,
data: U.toHex(v),
}
}));
};
if (hio.rawMode)
hio.io.onData = hio.io.onEvent;
hio.onSerial = (v, isErr) => {
if (!ws)
return;
ws.send(JSON.stringify({
op: "serial",
result: {
isError: isErr,
path: msg.arg.path,
data: U.toHex(v),
}
}));
};
return {};
});
case "send":
if (!hio.rawMode)
return null;
return hio.io.sendPacketAsync(U.fromHex(msg.arg.data))
.then(() => ({}));
case "talk":
return U.promiseMapAllSeries(msg.arg.cmds, (obj) => {
pxt.debug(`hid talk ${obj.cmd}`);
return hio.talkAsync(obj.cmd, U.fromHex(obj.data))
.then(res => ({ data: U.toHex(res) }));
});
case "sendserial":
return hio.sendSerialAsync(U.fromHex(msg.arg.data), msg.arg.isError);
case "list":
return hid.getHF2DevicesAsync()
.then(devices => { return { devices }; });
default: // unknown message
pxt.log(`unknown hid message ${msg.op}`);
return null;
}
})
.then(resp => {
if (!ws)
return;
pxt.debug(`hid: resp ${objToString(resp)}`);
ws.send(JSON.stringify({
op: msg.op,
id: msg.id,
result: resp
}));
}, error => {
pxt.log(`hid: error ${error.message}`);
if (!ws)
return;
ws.send(JSON.stringify({
result: {
errorMessage: error.message || "Error",
errorStackTrace: error.stack,
},
op: msg.op,
id: msg.id
}));
});
}
catch (e) {
console.log("ws hid error", e.stack);
}
});
ws.on('close', function (event) {
console.log('ws hid connection closed');
ws = null;
});
ws.on('error', function () {
console.log('ws hid connection closed');
ws = null;
});
}
let openSockets = {};
function startTCP(request, socket, body) {
let ws = new WebSocket(request, socket, body);
let netSockets = [];
ws.on('open', () => {
ws.send(JSON.stringify({ id: "ready" }));
});
ws.on('message', function (event) {
try {
let msg = JSON.parse(event.data);
pxt.debug(`tcp: msg ${msg.op}`); // , objToString(msg.arg))
Promise.resolve()
.then(() => {
let sock = openSockets[msg.arg.socket];
switch (msg.op) {
case "close":
sock.end();
let idx = netSockets.indexOf(sock);
if (idx >= 0)
netSockets.splice(idx, 1);
return {};
case "open":
return new Promise((resolve, reject) => {
const newSock = new net.Socket();
netSockets.push(newSock);
const id = pxt.U.guidGen();
newSock.on('error', err => {
if (ws)
ws.send(JSON.stringify({ op: "error", result: { socket: id, error: err.message } }));
});
newSock.connect(msg.arg.port, msg.arg.host, () => {
openSockets[id] = newSock;
resolve({ socket: id });
});
newSock.on('data', d => {
if (ws)
ws.send(JSON.stringify({ op: "data", result: { socket: id, data: d.toString("base64"), encoding: "base64" } }));
});
newSock.on('close', () => {
if (ws)
ws.send(JSON.stringify({ op: "close", result: { socket: id } }));
});
});
case "send":
sock.write(Buffer.from(msg.arg.data, msg.arg.encoding || "utf8"));
return {};
default: // unknown message
pxt.log(`unknown tcp message ${msg.op}`);
return null;
}
})
.then(resp => {
if (!ws)
return;
pxt.debug(`hid: resp ${objToString(resp)}`);
ws.send(JSON.stringify({
op: msg.op,
id: msg.id,
result: resp
}));
}, error => {
pxt.log(`hid: error ${error.message}`);
if (!ws)
return;
ws.send(JSON.stringify({
result: {
errorMessage: error.message || "Error",
errorStackTrace: error.stack,
},
op: msg.op,
id: msg.id
}));
});
}
catch (e) {
console.log("ws tcp error", e.stack);
}
});
function closeAll() {
console.log('ws tcp connection closed');
ws = null;
for (let s of netSockets) {
try {
s.end();
}
catch (e) { }
}
}
ws.on('close', closeAll);
ws.on('error', closeAll);
}
function startDebug(request, socket, body) {
let ws = new WebSocket(request, socket, body);
let dapjs;
ws.on('open', () => {
ws.send(JSON.stringify({ id: "ready" }));
});
ws.on('message', function (event) {
try {
let msg = JSON.parse(event.data);
if (!dapjs)
dapjs = require("dapjs");
let toHandle = msg.arg;
toHandle.op = msg.op;
console.log("DEBUGMSG", objToString(toHandle));
Promise.resolve()
.then(() => dapjs.handleMessageAsync(toHandle))
.then(resp => {
if (resp == null || typeof resp != "object")
resp = { response: resp };
console.log("DEBUGRESP", objToString(resp));
ws.send(JSON.stringify({
op: msg.op,
id: msg.id,
result: resp
}));
}, error => {
console.log("DEBUGERR", error.stack);
ws.send(JSON.stringify({
result: {
errorMessage: error.message || "Error",
errorStackTrace: error.stack,
},
op: msg.op,
id: msg.id
}));
});
}
catch (e) {
console.log("ws debug error", e.stack);
}
});
ws.on('close', function (event) {
console.log('ws debug connection closed');
ws = null;
});
ws.on('error', function () {
console.log('ws debug connection closed');
ws = null;
});
}
let wsserver = http.createServer();
wsserver.on('upgrade', function (request, socket, body) {
try {
if (WebSocket.isWebSocket(request)) {
console.log('ws connection at ' + request.url);
if (request.url == "/" + serveOptions.localToken + "/serial")
startSerial(request, socket, body);
else if (request.url == "/" + serveOptions.localToken + "/debug")
startDebug(request, socket, body);
else if (request.url == "/" + serveOptions.localToken + "/hid")
startHID(request, socket, body);
else if (request.url == "/" + serveOptions.localToken + "/tcp")
startTCP(request, socket, body);
else {
console.log('refused connection at ' + request.url);
socket.close(403);
}
}
}
catch (e) {
console.log('upgrade failed...');
}
});
return new Promise((resolve, reject) => {
wsserver.on("Error", reject);
wsserver.listen(wsPort, hostname, () => resolve());
});
}
function sendSerialMsg(msg) {
//console.log('sending ' + msg);
wsSerialClients.forEach(function (client) {
client.send(msg);
});
}
function initSerialMonitor() {
// TODO HID
}
// can use http://localhost:3232/streams/nnngzlzxslfu for testing
function streamPageTestAsync(id) {
return Cloud.privateGetAsync(id)
.then((info) => {
let html = pxt.docs.renderMarkdown({
template: expandDocFileTemplate("stream.html"),
markdown: "",
theme: pxt.appTarget.appTheme,
pubinfo: info,
filepath: "/" + id
});
return html;
});
}
function certificateTestAsync() {
return Promise.resolve(expandDocFileTemplate("certificates.html"));
}
// use http://localhost:3232/45912-50568-62072-42379 for testing
function scriptPageTestAsync(id) {
return Cloud.privateGetAsync(pxt.Cloud.parseScriptId(id))
.then((info) => {
// if running against old cloud, infer 'thumb' field
// can be removed after new cloud deployment
if (info.thumb !== undefined)
return info;
return Cloud.privateGetTextAsync(id + "/thumb")
.then(_ => {
info.thumb = true;
return info;
}, _ => {
info.thumb = false;
return info;
});
})
.then((info) => {
let infoA = info;
infoA.cardLogo = info.thumb
? Cloud.apiRoot + id + "/thumb"
: pxt.appTarget.appTheme.thumbLogo || pxt.appTarget.appTheme.cardLogo;
let html = pxt.docs.renderMarkdown({
template: expandDocFileTemplate(pxt.appTarget.appTheme.leanShare
? "leanscript.html" : "script.html"),
markdown: "",
theme: pxt.appTarget.appTheme,
pubinfo: info,
filepath: "/" + id
});
return html;
});
}
// use http://localhost:3232/pkg/microsoft/pxt-neopixel for testing
function pkgPageTestAsync(id) {
return pxt.packagesConfigAsync()
.then(config => pxt.github.repoAsync(id, config))
.then(repo => {
if (!repo)
return "Not found";
return Cloud.privateGetAsync("gh/" + id + "/text")
.then((files) => {
let info = JSON.parse(files["pxt.json"]);
info["slug"] = id;
info["id"] = "gh/" + id;
if (repo.status == pxt.github.GitRepoStatus.Approved)
info["official"] = "yes";
else
info["official"] = "";
const html = pxt.docs.renderMarkdown({
template: expandDocFileTemplate("package.html"),
markdown: files["README.md"] || "No `README.md`",
theme: pxt.appTarget.appTheme,
pubinfo: info,
filepath: "/pkg/" + id,
repo: { name: repo.name, fullName: repo.fullName, tag: "v" + info.version }
});
return html;
});
});
}
function readMdAsync(pathname, lang) {
if (!lang || lang == "en") {
const content = nodeutil.resolveMd(root, pathname);
if (content)
return Promise.resolve(content);
return Promise.resolve(`# Not found ${pathname}\nChecked:\n` + [docsDir].concat(dirs).concat(nodeutil.lastResolveMdDirs).map(s => "* ``" + s + "``\n").join(""));
}
else {
// ask makecode cloud for translations
const mdpath = pathname.replace(/^\//, '');
return pxt.Cloud.markdownAsync(mdpath, lang);
}
}
function resolveTOC(pathname) {
// find summary.md
let summarydir = pathname.replace(/^\//, '');
let presummarydir = "";
while (summarydir !== presummarydir) {
const summaryf = path.join(summarydir, "SUMMARY");
// find "closest summary"
const summaryMd = nodeutil.resolveMd(root, summaryf);
if (summaryMd) {
try {
return pxt.docs.buildTOC(summaryMd);
}
catch (e) {
pxt.log(`invalid ${summaryf} format - ${e.message}`);
pxt.log(e.stack);
}
break;
}
presummarydir = summarydir;
summarydir = path.dirname(summarydir);
}
// not found
pxt.log(`SUMMARY.md not found`);
return undefined;
}
const compiledCache = {};
async function compileScriptAsync(id) {
if (compiledCache[id])
return compiledCache[id];
const scrText = await Cloud.privateGetAsync(id + "/text");
const res = await pxt.simpleCompileAsync(scrText, {});
let r = "";
if (res.errors)
r = `throw new Error(${JSON.stringify(res.errors)})`;
else
r = res.outfiles["binary.js"];
compiledCache[id] = r;
return r;
}
exports.compileScriptAsync = compileScriptAsync;
function serveAsync(options) {
serveOptions = options;
if (!serveOptions.port)
serveOptions.port = 3232;
if (!serveOptions.wsPort)
serveOptions.wsPort = 3233;
if (!serveOptions.hostname)
serveOptions.hostname = "localhost";
setupRootDir();
const wsServerPromise = initSocketServer(serveOptions.wsPort, serveOptions.hostname);
if (serveOptions.serial)
initSerialMonitor();
const reqListener = async (req, res) => {
const error = (code, msg = null) => {
res.writeHead(code, { "Content-Type": "text/plain" });
res.end(msg || "Error " + code);
};
const sendJson = (v) => {
if (typeof v == "string") {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf8' });
res.end(v);
}
else {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf8' });
res.end(JSON.stringify(v));
}
};
const sendHtml = (s, code = 200) => {
res.writeHead(code, { 'Content-Type': 'text/html; charset=utf8' });
res.end(s.replace(/(<img [^>]* src=")(?:\/docs|\.)\/static\/([^">]+)"/g, function (f, pref, addr) { return pref + '/static/' + addr + '"'; }));
};
const sendFile = (filename) => {
try {
let stat = fs.statSync(filename);
res.writeHead(200, {
'Content-Type': U.getMime(filename),
'Content-Length': stat.size
});
fs.createReadStream(filename).pipe(res);
}
catch (e) {
error(404, "File missing: " + filename);
}
};
// Strip /app/hash-sig from URL.
// This can happen when the locally running backend is serving an uploaded target,
// but has been configured to route simulator urls to port 3232.
req.url = req.url.replace(/^\/app\/[0-9a-f]{40}(?:-[0-9a-f]{10})?(.*)$/i, "$1");
let uri = url.parse(req.url);
let pathname = decodeURI(uri.pathname);
const opts = querystring.parse(url.parse(req.url).query);
const htmlParams = {};
if (opts["lang"] || opts["forcelang"])
htmlParams["locale"] = (opts["lang"] || opts["forcelang"]);
if (pathname == "/") {
res.writeHead(301, { location: '/index.html' });
res.end();
return;
}
if (pathname == "/oauth-redirect") {
res.writeHead(301, { location: '/oauth-redirect.html' });
res.end();
return;
}
let elts = pathname.split("/").filter(s => !!s);
if (elts.some(s => s[0] == ".")) {
return error(400, "Bad path :-(\n");
}
// Strip leading version number
if (elts.length && /^v\d+/.test(elts[0])) {
elts.shift();
}
// Rebuild pathname without leading version number
pathname = "/" + elts.join("/");
const expandWebappHtml = (appname, html) => {
// Expand templates
html = expandHtml(html);
// Rewrite application resource references
html = html.replace(/src="(\/static\/js\/[^"]*)"/, (m, f) => `src="/${appname}${f}"`);
html = html.replace(/src="(\/static\/css\/[^"]*)"/, (m, f) => `src="/${appname}${f}"`);
return html;
};
const serveWebappFile = (webappName, webappPath) => {
const webappUri = url.parse(`http://127.0.0.1:3000/${webappPath}${uri.search || ""}`);
const request = http.get(webappUri, r => {
let body = "";
r.on("data", (chunk) => {
body += chunk;
});
r.on("end", () => {
if (body.includes("<title>Error</title>")) { // CRA development server returns this for missing files
res.writeHead(404, {
'Content-Type': 'text/html; charset=utf8',
});
res.write(body);
return res.end();
}
if (!webappPath || webappPath === "index.html") {
body = expandWebappHtml(webappName, body);
}
if (webappPath) {
res.writeHead(200, {
'Content-Type': U.getMime(webappPath),
});
}
else {
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf8',
});
}
res.write(body);
res.end();
});
});
request.on("error", (e) => {
console.error(`Error fetching ${webappUri.href} .. ${e.message}`);
error(500, e.message);
});
};
const webappNames = subwebapp_1.SUB_WEBAPPS.filter(w => w.localServeEndpoint).map(w => w.localServeEndpoint);
const webappIdx = webappNames.findIndex(s => new RegExp(`^-{0,3}${s}$`).test(elts[0] || ''));
if (webappIdx >= 0) {
const webappName = webappNames[webappIdx];
const webappPath = pathname.split("/").slice(2).join('/'); // remove /<webappName>/ from path
return serveWebappFile(webappName, webappPath);
}
if (elts[0] == "api") {
if (elts[1] == "streams") {
let trg = Cloud.apiRoot + req.url.slice(5);
res.setHeader("Location", trg);
error(302, "Redir: " + trg);
return;
}
if (elts[1] == "immreader") {
let trg = Cloud.apiRoot + elts[1];
res.setHeader("Location", trg);
error(302, "Redir: " + trg);
return;
}
if (elts[1] == "store") {
return await handleApiStoreRequestAsync(req, res, elts.slice(2));
}
if (/^\d\d\d[\d\-]*$/.test(elts[1]) && elts[2] == "js") {
return compileScriptAsync(elts[1])
.then(data => {
res.writeHead(200, { 'Content-Type': 'application/javascript' });
res.end(data);
}, err => {
error(500);
console.log(err.stack);
});
}
if (!options.noauth && !isAuthorizedLocalRequest(req)) {
error(403);
return null;
}
return handleApiAsync(req, res, elts)
.then(sendJson, err => {
if (err.statusCode) {
error(err.statusCode, err.message || "");
console.log("Error " + err.statusCode);
}
else {
error(500);
console.log(err.stack);
}
});
}
if (elts[0] == "icon") {
const name = path.join(userProjectsDir, elts[1], "icon.jpeg");
return existsAsync(name)
.then(exists => exists ? sendFile(name) : error(404));
}
if (elts[0] == "assets") {
if (/^[a-z0-9\-_]/.test(elts[1]) && !/[\/\\]/.test(elts[1]) && !/^[.]/.test(elts[2])) {
let filename = path.join(userProjectsDir, elts[1], "assets", elts[2]);
if (nodeutil.fileExistsSync(filename)) {
return sendFile(filename);
}
else {
return error(404, "Asset not found");
}
}
else {
return error(400, "Invalid asset path");
}
}
if (elts[0] == "simx" && serveOptions.backport) {
// Proxy requests for simulator extensions to the locally running backend.
// Should only get here when the backend is running locally and configured to serve the simulator from the cli (via LOCAL_SIM_PORT setting).
const passthruOpts = {
hostname: uri.hostname,
port: serveOptions.backport,
path: uri.path,
method: req.method,
headers: req.headers
};
const passthruReq = http.request(passthruOpts, passthruRes => {
res.writeHead(passthruRes.statusCode, passthruRes.headers);
passthruRes.pipe(res);
});
passthruReq.on("error", e => {
console.error(`Error proxying request to port ${serveOptions.backport} .. ${e.message}`);
return error(500, e.message);
});
return req.pipe(passthruReq);
}
if (options.packaged) {
let filename = path.resolve(path.join(packagedDir, pathname));
if (nodeutil.fileExistsSync(filename)) {
return sendFile(filename);
}
else {
return error(404, "Packaged file not found");
}
}
if (pathname.slice(0, pxt.appTarget.id.length + 2) == "/" + pxt.appTarget.id + "/") {
res.writeHead(301, { location: req.url.slice(pxt.appTarget.id.length + 1) });
res.end();
return;
}
let publicDir = path.join(nodeutil.pxtCoreDir, "webapp/public");
if (pathname == "/--embed" || pathname === "/---embed") {
sendFile(path.join(publicDir, 'embed.js'));
return;
}
if (pathname == "/--run") {
sendFile(path.join(publicDir, 'run.html'));
return;
}
if (pathname == "/--multi") {
sendFile(path.join(publicDir, 'multi.html'));
return;
}
if (pathname == "/--asseteditor") {
sendFile(path.join(publicDir, 'asseteditor.html'));
return;
}
for (const subapp of subwebapp_1.SUB_WEBAPPS) {
if (subapp.localServeWebConfigUrl && pathname === `/--${subapp.name}`) {
sendFile(path.join(publicDir, `${subapp.name}.html`));
return;
}
}
if (/\/-[-]*docs.*$/.test(pathname)) {
sendFile(path.join(publicDir, 'docs.html'));
return;
}
if (pathname == "/--codeembed") {
// http://localhost:3232/--codeembed#pub:20467-26471-70207-51013
sendFile(path.join(publicDir, 'codeembed.html'));
return;
}
if (!!pxt.Cloud.parseScriptId(pathname)) {
scriptPageTestAsync(pathname)
.then(sendHtml)
.catch(() => error(404, "Script not found"));
return;
}
if (/^\/(pkg|package)\/.*$/.test(pathname)) {
pkgPageTestAsync(pathname.replace(/^\/[^\/]+\//, ""))
.then(sendHtml)
.catch(() => error(404, "Packaged file not found"));
return;
}
if (elts[0] == "streams") {
streamPageTestAsync(elts[0] + "/" + elts[1])
.then(sendHtml);
return;
}
if (elts[0] == "certificates") {
certificateTestAsync().then(sendHtml);
return;
}
if (/\.js\.map$/.test(pathname)) {
error(404, "map files disabled");
return;
}
let dd = dirs;
let mm = /^\/(cdn|parts|sim|doccdn|blb|trgblb)(\/.*)/.exec(pathname);
if (mm) {
pathname = mm[2];
}
else if (U.startsWith(pathname, "/docfiles/")) {
pathname = pathname.slice(10);
dd = docfilesdirs;
}
for (let dir of dd) {
let filename = path.resolve(path.join(dir, pathname));
if (nodeutil.fileExistsSync(filename)) {
if (/\.html$/.test(filename)) {
let html = expandHtml(fs.readFileSync(filename, "utf8"), htmlParams);
sendHtml(html);
}
else {
sendFile(filename);
}
return;
}
}
// Look for an .html file corresponding to `/---<pathname>`
// Handles serving of `trg-<target>.sim.local:<port>/---simulator`
let match = /^\/?---?(.*)/.exec(pathname);
if (match && match[1]) {
const htmlPathname = `/${match[1]}.html`;
for (let dir of dd) {
const filename = path.resolve(path.join(dir, htmlPathname));
if (nodeutil.fileExistsSync(filename)) {
const html = expandHtml(fs.readFileSync(filename, "utf8"), htmlParams);
return sendHtml(html);
}
}
}
if (/simulator\.html/.test(pathname)) {
// check for simx urls, e.g.:
// /simulator.html/simx/microbit-apps/display-shield/-/index.html
if (/simulator\.html\/simx\//.test(pathname)) {
res.writeHead(302, { location: `https://trg-${pxt.appTarget.id}.userpxt.io/simx${pathname.split("simx").pop()}` });
}
else {
// Special handling for missing simulator: redirect to the live sim
res.writeHead(302, { location: `https://trg-${pxt.appTarget.id}.userpxt.io/---simulator` });
}
res.end();
return;
}
// redirect
let redirectFile = path.join(docsDir, pathname + "-ref.json");
if (nodeutil.fileExistsSync(redirectFile)) {
const redir = nodeutil.readJson(redirectFile);
res.writeHead(301, { location: redir["redirect"] });
res.end();
return;
}
let webFile = path.join(docsDir, pathname);
if (!nodeutil.fileExistsSync(webFile)) {
if (nodeutil.fileExistsSync(webFile + ".html")) {
webFile += ".html";
pathname += ".html";
}
else {
webFile = "";
}
}
if (webFile) {
if (/\.html$/.test(webFile)) {
let html = expandHtml(fs.readFileSync(webFile, "utf8"), htmlParams);
sendHtml(html);
}
else {
sendFile(webFile);
}
}
else {
const m = /^\/(v\d+)(.*)/.exec(pathname);
if (m)
pathname = m[2];
const lang = (opts["translate"] && ts.pxtc.Util.TRANSLATION_LOCALE)
|| opts["lang"]
|| opts["forcelang"];
readMdAsync(pathname, lang)
.then(md => {
const mdopts = {
template: expandDocFileTemplate("docs.html"),
markdown: md,
theme: pxt.appTarget.appTheme,
filepath: pathname,
TOC: resolveTOC(pathname),
pubinfo: {
locale: lang,
crowdinproject: pxt.appTarget.appTheme.crowdinProject
}
};
if (opts["translate"])
mdopts.pubinfo["incontexttranslations"] = "1";
const html = pxt.docs.renderMarkdown(mdopts);
sendHtml(html, U.startsWith(md, "# Not found") ? 404 : 200);
});
}
return;
};
const canUseHttps = serveOptions.https && process.env["HTTPS_KEY"] && process.env["HTTPS_CERT"];
const httpsServerOptions = { cert: process.env["HTTPS_CERT"], key: process.env["HTTPS_KEY"] };
const server = canUseHttps ? https.createServer(httpsServerOptions, reqListener) : http.createServer(reqListener);
// if user has a server.js file, require it
const serverjs = path.resolve(path.join(root, 'built', 'server.js'