makecode-core
Version:
MakeCode (PXT) - web-cached build tool
233 lines • 9.59 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.downloadAsync = exports.httpGetJsonAsync = exports.httpGetTextAsync = exports.requestAsync = void 0;
const host_1 = require("./host");
const xmldom_1 = require("@xmldom/xmldom");
function requestAsync(options) {
log("Download " + options.url);
return (0, host_1.host)().requestAsync(options).then(resp => {
if (resp.statusCode != 200 &&
resp.statusCode != 304 &&
!options.allowHttpErrors) {
let msg = `Bad HTTP status code: ${resp.statusCode} at ${options.url}; message: ${(resp.text || "").slice(0, 500)}`;
let err = new Error(msg);
err.statusCode = resp.statusCode;
return Promise.reject(err);
}
if (resp.text &&
/application\/json/.test(resp.headers["content-type"]))
resp.json = JSON.parse(resp.text);
return resp;
});
}
exports.requestAsync = requestAsync;
function httpGetTextAsync(url) {
return requestAsync({ url: url }).then(resp => resp.text);
}
exports.httpGetTextAsync = httpGetTextAsync;
function httpGetJsonAsync(url) {
return requestAsync({ url: url }).then(resp => resp.json);
}
exports.httpGetJsonAsync = httpGetJsonAsync;
function resolveUrl(root, path) {
if (path[0] == "/") {
return root.replace(/(:\/\/[^\/]*)\/.*/, (x, y) => y) + path;
}
return path;
}
async function parseWebConfigAsync(url) {
// html
const html = await httpGetTextAsync(url);
const lines = html.split("\n");
let rawConfig = "";
let openBrackets = 0;
for (const line of lines) {
if (line.indexOf("var pxtConfig =") !== -1) {
openBrackets++;
rawConfig += line.slice(line.indexOf("{"));
}
else if (openBrackets) {
if (line.indexOf("{") !== -1) {
openBrackets++;
}
if (line.indexOf("}") !== -1) {
openBrackets--;
if (openBrackets === 0) {
rawConfig += line.slice(0, line.indexOf("}") + 1);
break;
}
}
rawConfig += line;
}
}
const config = rawConfig && JSON.parse(rawConfig);
if (config) {
config.rootUrl = url;
config.files = {};
const m = /manifest="([^"]+)"/.exec(html);
if (m)
config.manifestUrl = resolveUrl(url, m[1]);
}
return config;
}
function log(msg) {
console.log(msg);
}
async function downloadAsync(cache, webAppUrl, forceCheckUpdates = false) {
var _a;
const infoBuf = await cache.getAsync(webAppUrl + "-info");
const info = infoBuf
? JSON.parse((0, host_1.host)().bufferToString(infoBuf))
: {};
const fetchTargetConfig = async (cdnUrl, target, version) => {
const currentDate = new Date();
const year = currentDate.getUTCFullYear();
const month = `${currentDate.getUTCMonth()}`.padStart(2, "0");
const day = `${currentDate.getUTCDay()}`.padStart(2, "0");
const cacheBustId = `${year}${month}${day}`;
const resp = await requestAsync({
url: `${cdnUrl}/api/config/${target}/targetconfig${version ? `/v${version}` : ""}?cdn=${cacheBustId}`
});
return JSON.parse(resp.text);
};
if (forceCheckUpdates && info.manifest && info.webConfig) {
let needsUpdate = false;
if (!info.updateCheckedAt ||
Date.now() - info.updateCheckedAt > 24 * 3600 * 1000) {
info.updateCheckedAt = Date.now();
await saveInfoAsync(); // save last check time *before* checking - in case user hits ctrl-c we don't want another build to hang again
try {
log("Checking for updates (only happens once daily)...");
needsUpdate = await hasNewManifestAsync();
if (!needsUpdate) {
// fetch new target config as that is 'live'
const targetConfig = await fetchTargetConfig(info.webConfig.cdnUrl, info.webConfig.targetId, info.targetVersion);
if (targetConfig) {
info.targetConfig = targetConfig;
await saveInfoAsync();
}
}
}
catch (e) {
log(`Error checking for updates; will try again tomorrow (use -u flag to force); ${e.message}`);
}
}
if (!needsUpdate)
return loadFromCacheAsync();
}
else {
if (!(await hasNewManifestAsync()))
return loadFromCacheAsync();
}
log("Download new webapp");
const cfg = await parseWebConfigAsync(webAppUrl);
if (!cfg.manifestUrl)
cfg.manifestUrl = webAppUrl; // use index.html if no manifest
if (info.manifestUrl != cfg.manifestUrl || !info.webConfig) {
info.manifestUrl = cfg.manifestUrl;
info.manifestEtag = null;
info.cdnUrl = cfg.cdnUrl;
info.webConfig = cfg;
await hasNewManifestAsync();
}
info.versionNumber = (info.versionNumber || 0) + 1;
info.updateCheckedAt = Date.now();
await saveFileAsync("pxtworker.js");
const targetJsonBuf = await saveFileAsync("target.json");
const targetJson = JSON.parse((0, host_1.host)().bufferToString(targetJsonBuf));
info.targetVersion = (_a = targetJson === null || targetJson === void 0 ? void 0 : targetJson.versions) === null || _a === void 0 ? void 0 : _a.target;
const targetConfig = await fetchTargetConfig(cfg.cdnUrl, cfg.targetId, info.targetVersion);
info.targetConfig = targetConfig;
if (cache.rootPath) {
info.simKey = webAppUrl + "-sim.html";
await downloadPageAndDependenciesAsync(cfg.simUrl, info.simKey);
info.assetEditorKey = webAppUrl + "-asseteditor.html";
await downloadPageAndDependenciesAsync(webAppUrl + "---asseteditor", info.assetEditorKey);
}
delete info.cachedAssetEditorKey;
delete info.cachedSimulatorKey;
return loadFromCacheAsync();
function saveInfoAsync() {
return cache.setAsync(webAppUrl + "-info", (0, host_1.host)().stringToBuffer(JSON.stringify(info)));
}
async function loadFromCacheAsync() {
await saveInfoAsync();
const res = {
cache,
versionNumber: info.versionNumber || 0,
cdnUrl: info.cdnUrl,
website: webAppUrl,
simUrl: info.simKey
? cache.rootPath + "/" + cache.expandKey(info.simKey)
: null,
pxtWorkerJs: (0, host_1.host)().bufferToString(await cache.getAsync(webAppUrl + "-pxtworker.js")),
targetJson: JSON.parse((0, host_1.host)().bufferToString(await cache.getAsync(webAppUrl + "-target.json"))),
webConfig: info.webConfig,
targetConfig: info.targetConfig
};
return res;
}
async function saveFileAsync(name) {
const resp = await requestAsync({ url: cfg.pxtCdnUrl + name });
await cache.setAsync(webAppUrl + "-" + name, resp.buffer);
return resp.buffer;
}
async function hasNewManifestAsync() {
if (!info.manifestUrl || !info.webConfig)
return true;
const resp = await requestAsync({
url: info.manifestUrl,
headers: info.manifestEtag
? {
"if-none-match": info.manifestEtag,
}
: {},
});
if (resp.statusCode == 304) {
info.updateCheckedAt = Date.now();
return false;
}
info.manifestEtag = resp.headers["etag"];
if (resp.text == info.manifest) {
info.updateCheckedAt = Date.now();
return false;
}
info.manifest = resp.text;
return true;
}
async function downloadPageAndDependenciesAsync(url, cacheKey) {
let pageText = await httpGetTextAsync(url);
const dom = new xmldom_1.DOMParser().parseFromString(pageText, "text/html");
const additionalUrls = [];
const urlKeyMap = {};
for (const script of dom.getElementsByTagName("script")) {
if (!script.hasAttribute("src"))
continue;
const url = script.getAttribute("src");
if (!url.startsWith(info.cdnUrl) || !url.endsWith(".js"))
continue;
additionalUrls.push(url);
urlKeyMap[url] = webAppUrl + "-" + url.replace(/.*\//, "");
script.setAttribute("src", cache.expandKey(urlKeyMap[url]));
}
for (const link of dom.getElementsByTagName("link")) {
if (!link.hasAttribute("href"))
continue;
const url = link.getAttribute("href");
if (!url.startsWith(info.cdnUrl) || !url.endsWith(".css"))
continue;
additionalUrls.push(url);
urlKeyMap[url] = webAppUrl + "-" + url.replace(/.*\//, "");
link.setAttribute("href", cache.expandKey(urlKeyMap[url]));
}
pageText = new xmldom_1.XMLSerializer().serializeToString(dom);
pageText = pageText.replace(/ manifest=/, " x-manifest=");
await cache.setAsync(cacheKey, (0, host_1.host)().stringToBuffer(pageText));
for (let url of additionalUrls) {
const resp = await requestAsync({ url });
await cache.setAsync(urlKeyMap[url], resp.buffer);
}
}
}
exports.downloadAsync = downloadAsync;
//# sourceMappingURL=downloader.js.map