UNPKG

makecode-core

Version:

MakeCode (PXT) - web-cached build tool

233 lines 9.59 kB
"use strict"; 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