makecode-core
Version:
MakeCode (PXT) - web-cached build tool
857 lines (856 loc) • 32.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAssetEditorHTML = exports.getWebConfig = exports.getSimHTML = exports.shareCommand = exports.addCommand = exports.stackCommand = exports.searchCommand = exports.getTargetConfigAsync = exports.getAppTargetAsync = exports.listHardwareVariantsAsync = exports.initCommand = exports.installCommand = exports.buildCommandOnce = exports.resolveProject = exports.clearProjectCache = exports.cleanCommand = exports.downloadCommand = exports.applyGlobalOptions = void 0;
const path = require("path");
const chalk = require("chalk");
const xmldom_1 = require("@xmldom/xmldom");
const mkc = require("./mkc");
const files = require("./files");
const downloader = require("./downloader");
const loader_1 = require("./loader");
const mkc_1 = require("./mkc");
const stackresolver_1 = require("./stackresolver");
const files_1 = require("./files");
const host_1 = require("./host");
const share_1 = require("./share");
async function downloadProjectAsync(id) {
id = id.replace(/.*\//, "");
const url = mkc.cloudRoot + id + "/text";
const files = await downloader.httpGetJsonAsync(url);
for (let fn of Object.keys(files)) {
if (/\//.test(fn))
continue;
await (0, host_1.host)().writeFileAsync(fn, files[fn]);
}
msg("downloaded.");
}
async function buildOnePrj(opts, prj) {
try {
const simpleOpts = {
native: opts.native,
computeUsedParts: opts.buildSimJsInfo,
emitBreakpoints: opts.emitBreakpoints
};
const res = await prj.buildAsync(simpleOpts);
const msgToString = (diagnostic) => {
const category = diagnostic.category == 1
? chalk.red("error")
: diagnostic.category == 2
? chalk.yellowBright("warning")
: "message";
return `${category} TS${diagnostic.code}: ${diagnostic.messageText}\n`;
};
let output = "";
for (let diagnostic of res.diagnostics) {
let pref = "";
if (diagnostic.fileName)
pref = `${diagnostic.fileName}(${diagnostic.line + 1},${diagnostic.column + 1}): `;
if (typeof diagnostic.messageText == "string")
output += pref + msgToString(diagnostic);
else {
for (let chain = diagnostic.messageText; chain; chain = chain.next) {
output += pref + msgToString(chain);
}
}
}
if (output)
console.log(output.replace(/\n$/, ""));
return res;
}
catch (e) {
error("Exception: " + e.stack);
return null;
}
}
function info(msg) {
console.log(chalk.blueBright(msg));
}
function msg(msg) {
console.log(chalk.green(msg));
}
function error(msg) {
console.error(chalk.red(msg));
}
function applyGlobalOptions(opts) {
if (opts.noColors)
chalk.level = 0;
else if (opts.colors && !chalk.level)
chalk.level = 1;
else if ((0, host_1.host)().getEnvironmentVariable("GITHUB_WORKFLOW"))
chalk.level = 1;
}
exports.applyGlobalOptions = applyGlobalOptions;
async function downloadCommand(URL, opts) {
applyGlobalOptions(opts);
await downloadProjectAsync(URL);
}
exports.downloadCommand = downloadCommand;
async function cleanCommand(opts) {
applyGlobalOptions(opts);
for (const dir of ["built", "pxt_modules"]) {
if (await (0, host_1.host)().existsAsync(dir)) {
msg(`deleting ${dir} folder`);
await (0, host_1.host)().rmdirAsync(dir, { recursive: true, force: true });
}
}
msg("run `mkc init` again to setup your project");
}
exports.cleanCommand = cleanCommand;
let prjCache = {};
async function clearProjectCache() {
var _a, _b;
for (const prjdir of Object.keys(prjCache)) {
const prj = prjCache[prjdir];
(_b = (_a = prj.service) === null || _a === void 0 ? void 0 : _a.dispose) === null || _b === void 0 ? void 0 : _b.call(_a);
}
prjCache = {};
}
exports.clearProjectCache = clearProjectCache;
function getCachedProject(prjdir) {
if (!prjCache[prjdir]) {
prjCache[prjdir] = new mkc.Project(prjdir);
}
return prjCache[prjdir];
}
async function resolveProject(opts, quiet = false) {
var _a;
const prjdir = await files.findProjectDirAsync();
if (!prjdir) {
error(`could not find "pxt.json" file`);
(0, host_1.host)().exitWithStatus(1);
}
if (!opts.configPath) {
const cfgFolder = await files.findParentDirWithAsync(prjdir, "mkc.json");
if (cfgFolder)
opts.configPath = path.join(cfgFolder, "mkc.json");
}
log(`using project: ${prjdir}/pxt.json`);
const prj = getCachedProject(prjdir);
if (opts.configPath) {
log(`using config: ${opts.configPath}`);
prj.mkcConfig = await readCfgAsync(opts.configPath, quiet);
}
await prj.loadEditorAsync(!!opts.update);
let version = "???";
try {
const appTarget = await prj.service.languageService.getAppTargetAsync();
version = (_a = appTarget === null || appTarget === void 0 ? void 0 : appTarget.versions) === null || _a === void 0 ? void 0 : _a.target;
}
catch (_b) { }
log(`using editor: ${prj.mkcConfig.targetWebsite} v${version}`);
if (opts.debug)
await prj.service.languageService.enableDebugAsync();
if (opts.compileFlags) {
await prj.service.languageService.setCompileSwitchesAsync(opts.compileFlags);
}
prj.writePxtModules = !!opts.pxtModules;
if (opts.linkPxtModules) {
prj.writePxtModules = true;
prj.linkPxtModules = true;
}
else if (opts.symlinkPxtModules) {
prj.writePxtModules = true;
prj.symlinkPxtModules = true;
}
return prj;
function log(msg) {
if (!quiet)
info(msg);
}
}
exports.resolveProject = resolveProject;
async function buildCommandOnce(opts) {
const prj = await resolveProject(opts);
await prj.service.languageService.enableExperimentalHardwareAsync();
const hwVariants = await prj.service.getHardwareVariantsAsync();
const appTarget = await prj.service.languageService.getAppTargetAsync();
const targetId = appTarget.id;
let moreHw = [];
const outputs = [];
if (opts.buildSimJsInfo) {
opts.javaScript = true;
}
if (opts.hw) {
const hws = opts.hw.split(/[\s,;]+/);
selectHW(hws[0]);
moreHw = hws.slice(1);
}
if (!opts.javaScript || opts.hw)
opts.native = true;
else
opts.native = false;
if (opts.native && hwVariants.length) {
await prj.guessHwVariantAsync();
infoHW();
}
outputs.push(prj.outputPrefix);
const compileRes = await buildOnePrj(opts, prj);
const firmwareName = compileRes.success && ["binary.uf2", "binary.hex", "binary.elf"].filter(f => !!compileRes.outfiles[f])[0];
if (compileRes.success && opts.deploy) {
if (!firmwareName) {
// something went wrong here
error(`firmware missing from built files (${Object.keys(compileRes.outfiles).join(", ")})`);
}
else {
const compileInfo = appTarget.compile;
const drives = await (0, host_1.host)().getDeployDrivesAsync(compileInfo);
if (drives.length == 0) {
msg("cannot find any drives to deploy to");
}
else {
const firmware = compileRes.outfiles[firmwareName];
const encoding = firmwareName == "binary.hex" ? "utf8" : "base64";
msg(`copying ${firmwareName} to ` + drives.join(", "));
const writeHexFile = (drivename) => {
return (0, host_1.host)().writeFileAsync(path.join(drivename, firmwareName), firmware, encoding)
.then(() => info(" wrote to " + drivename))
.catch(() => error(` failed writing to ${drivename}`));
};
for (const p of drives.map(writeHexFile))
await p;
}
}
}
let success = compileRes.success;
if (success && opts.monoRepo) {
const dirs = await (0, files_1.monoRepoConfigsAsync)(".");
info(`mono-repo: building ${dirs.length} projects`);
for (const fullpxtjson of dirs) {
if (fullpxtjson.startsWith("pxt_modules"))
continue;
const fulldir = path.dirname(fullpxtjson);
info(`build ${fulldir}`);
const prj0 = await prj.mkChildProjectAsync(fulldir);
const cfg = await prj0.readPxtConfig();
if (cfg.supportedTargets &&
cfg.supportedTargets.indexOf(targetId) < 0) {
info(`skipping due to supportedTargets`);
continue;
}
const res = await buildOnePrj(opts, prj0);
if (!res.success)
success = false;
}
}
else if (success && moreHw.length) {
for (const hw of moreHw) {
selectHW(hw);
infoHW();
outputs.push(prj.outputPrefix);
await buildOnePrj(opts, prj);
}
const uf2s = [];
for (const folder of outputs) {
try {
uf2s.push(await (0, host_1.host)().readFileAsync(path.join(folder, "binary.uf2")));
}
catch (_a) { }
}
if (uf2s.length > 1) {
const total = concatUint8Arrays(uf2s);
const fn = "built/combined.uf2";
info(`combining ${uf2s.length} UF2 files into ${fn} (${Math.round(total.length / 1024)}kB)`);
await (0, host_1.host)().writeFileAsync(fn, total);
}
}
if (compileRes && outputs.length && firmwareName) {
compileRes.binaryPath = outputs[0] + "/" + firmwareName;
}
if (compileRes && opts.buildSimJsInfo) {
compileRes.simJsInfo = await prj.buildSimJsInfoAsync(compileRes);
compileRes.simJsInfo.parts = compileRes.usedParts;
}
if (success) {
msg("Build OK");
if (opts.watch)
return compileRes;
else
(0, host_1.host)().exitWithStatus(0);
}
else {
error("Build failed");
if (opts.watch)
return compileRes;
else
(0, host_1.host)().exitWithStatus(1);
}
return null;
function hwid(cfg) {
return cfg.name.replace(/hw---/, "");
}
function selectHW(hw0) {
const hw = hw0.toLowerCase();
const selected = hwVariants.filter(cfg => {
return (cfg.name.toLowerCase() == hw ||
hwid(cfg).toLowerCase() == hw ||
cfg.card.name.toLowerCase() == hw);
});
if (!selected.length) {
error(`No such HW id: ${hw0}`);
msg(`Available hw:`);
for (let cfg of hwVariants) {
msg(`${hwid(cfg)}, ${cfg.card.name} - ${cfg.card.description}`);
}
(0, host_1.host)().exitWithStatus(1);
}
prj.hwVariant = hwid(selected[0]);
}
function infoHW() {
info(`using hwVariant: ${prj.mainPkg.mkcConfig.hwVariant} (target ${targetId})`);
if (!opts.alwaysBuilt)
prj.outputPrefix = "built/" + prj.mainPkg.mkcConfig.hwVariant;
}
}
exports.buildCommandOnce = buildCommandOnce;
async function installCommand(opts) {
applyGlobalOptions(opts);
if (!await (0, host_1.host)().existsAsync("pxt.json")) {
error("missing pxt.json");
(0, host_1.host)().exitWithStatus(1);
}
opts.pxtModules = true;
const prj = await resolveProject(opts);
prj.mainPkg = null;
if (opts.monoRepo) {
const dirs = await (0, files_1.monoRepoConfigsAsync)(".");
info(`mono-repo: building ${dirs.length} projects`);
for (const fullpxtjson of dirs) {
if (fullpxtjson.startsWith("pxt_modules"))
continue;
const fulldir = path.dirname(fullpxtjson);
info(`install ${fulldir}`);
const prj0 = await prj.mkChildProjectAsync(fulldir);
await prj0.maybeWritePxtModulesAsync();
}
}
else {
await prj.maybeWritePxtModulesAsync();
}
}
exports.installCommand = installCommand;
async function initCommand(template, deps, opts) {
applyGlobalOptions(opts);
if (!await (0, host_1.host)().existsAsync("pxt.json")) {
if (opts.importUrl) {
await downloadProjectAsync(opts.importUrl);
}
else {
if (!template) {
error("missing template");
(0, host_1.host)().exitWithStatus(1);
}
const target = loader_1.descriptors.find(t => t.id === template);
if (!target) {
error(`template not found`);
(0, host_1.host)().exitWithStatus(1);
}
msg(`initializing project for ${target.name}`);
msg("saving main.ts");
await (0, host_1.host)().writeFileAsync("main.ts", "// add code here", "utf8");
msg("saving pxt.json");
await (0, host_1.host)().writeFileAsync("pxt.json", JSON.stringify({
name: "my-project",
version: "0.0.0",
files: ["main.ts"],
supportedTargets: [target.targetId],
dependencies: target.dependencies ||
(target.corepkg && { [target.corepkg]: "*" }) ||
{},
testDependencies: target.testDependencies || {},
}, null, 4));
await (0, host_1.host)().writeFileAsync("mkc.json", JSON.stringify({
targetWebsite: target.website,
links: {},
}, null, 4), "utf8");
}
}
else {
if (template || opts.importUrl) {
error("directory is not empty, cannot apply template");
(0, host_1.host)().exitWithStatus(1);
}
}
const vscodeSettings = ".vscode/settings.json";
if (opts.vscodeProject && !await (0, host_1.host)().existsAsync(vscodeSettings)) {
if (!await (0, host_1.host)().existsAsync(".vscode"))
await (0, host_1.host)().mkdirAsync(".vscode");
await (0, host_1.host)().writeFileAsync(vscodeSettings, JSON.stringify({
"editor.formatOnType": true,
"files.autoSave": "afterDelay",
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/built/**": true,
"**/node_modules/**": true,
"**/yotta_modules/**": true,
"**/yotta_targets": true,
"**/pxt_modules/**": true,
"**/.pxt/**": true
},
"files.associations": {
"*.blocks": "html",
"*.jres": "json"
},
"search.exclude": {
"**/built": true,
"**/node_modules": true,
"**/yotta_modules": true,
"**/yotta_targets": true,
"**/pxt_modules": true,
"**/.pxt": true
},
"files.exclude": {
"**/pxt_modules": true,
"**/.pxt": true,
"**/mkc.json": true
}
}, null, 4));
}
const vscodeExtensions = ".vscode/extensions.json";
if (opts.vscodeProject && !await (0, host_1.host)().existsAsync(vscodeExtensions)) {
if (!await (0, host_1.host)().existsAsync(".vscode"))
await (0, host_1.host)().mkdirAsync(".vscode");
await (0, host_1.host)().writeFileAsync(vscodeExtensions, JSON.stringify({
recommendations: [
"ms-edu.pxt-vscode-web"
]
}, null, 4));
}
const gitignore = ".gitignore";
if (opts.gitIgnore && !await (0, host_1.host)().existsAsync(gitignore)) {
msg(`saving ${gitignore}`);
await (0, host_1.host)().writeFileAsync(gitignore, `# MakeCode
built
node_modules
yotta_modules
yotta_targets
pxt_modules
.pxt
_site
*.db
*.tgz
.header.json
.simstate.json`);
}
if (!await (0, host_1.host)().existsAsync("tsconfig.json")) {
msg("saving tsconfig.json");
await (0, host_1.host)().writeFileAsync("tsconfig.json", JSON.stringify({
compilerOptions: {
target: "ES5",
noImplicitAny: true,
outDir: "built",
rootDir: ".",
},
include: ["**/*.ts"],
exclude: ["built/**", "pxt_modules/**/*test.ts"],
}, null, 4), "utf8");
}
const prettierrc = ".prettierrc";
if (!await (0, host_1.host)().existsAsync(prettierrc)) {
msg(`saving ${prettierrc}`);
await (0, host_1.host)().writeFileAsync(prettierrc, JSON.stringify({
arrowParens: "avoid",
semi: false,
tabWidth: 4,
}));
}
const gh = ".github/workflows/makecode.yml";
if (!await (0, host_1.host)().existsAsync(gh)) {
if (!await (0, host_1.host)().existsAsync(".github"))
await (0, host_1.host)().mkdirAsync(".github");
if (!await (0, host_1.host)().existsAsync(".github/workflows"))
await (0, host_1.host)().mkdirAsync(".github/workflows");
msg(`saving ${gh}`);
await (0, host_1.host)().writeFileAsync(gh, `name: MakeCode Build
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- run: npx makecode
`);
}
opts.pxtModules = true;
const prj = await resolveProject(opts);
if (!await (0, host_1.host)().existsAsync("mkc.json")) {
msg("saving mkc.json");
await (0, host_1.host)().writeFileAsync("mkc.json", mkc.stringifyConfig(prj.mainPkg.mkcConfig), "utf8");
}
for (const dep of deps)
await addDependency(prj, dep, undefined);
prj.mainPkg = null;
prj.writePxtModules = true;
await prj.maybeWritePxtModulesAsync();
msg(`project ready, run "makecode -d" to build and deploy`);
}
exports.initCommand = initCommand;
async function listHardwareVariantsAsync(opts) {
const prj = await resolveProject(opts);
await prj.service.languageService.enableExperimentalHardwareAsync();
return await prj.service.getHardwareVariantsAsync();
}
exports.listHardwareVariantsAsync = listHardwareVariantsAsync;
async function getAppTargetAsync(opts) {
const prj = await resolveProject(opts);
return await prj.service.languageService.getAppTargetAsync();
}
exports.getAppTargetAsync = getAppTargetAsync;
async function getTargetConfigAsync(opts) {
const prj = await resolveProject(opts);
return await prj.service.languageService.getTargetConfigAsync();
}
exports.getTargetConfigAsync = getTargetConfigAsync;
async function jacdacMakeCodeExtensions() {
let data = [];
try {
const r = await (0, host_1.host)().requestAsync({
url: "https://raw.githubusercontent.com/microsoft/jacdac/main/services/makecode-extensions.json"
});
data = JSON.parse(r.text);
}
catch (e) { }
return data;
}
function join(...parts) {
return parts.filter(p => !!p).join("/");
}
// parse https://github.com/[company]/[project](/filepath)(#tag)
function parseRepoId(repo) {
if (!repo)
return undefined;
// clean out whitespaces
repo = repo.trim();
// trim trailing /
repo = repo.replace(/\/$/, "");
// convert github pages into github repo
const mgh = /^https:\/\/([^./#]+)\.github\.io\/([^/#]+)\/?$/i.exec(repo);
if (mgh)
repo = `github:${mgh[1]}/${mgh[2]}`;
repo = repo.replace(/^github:/i, "");
repo = repo.replace(/^https:\/\/github\.com\//i, "");
repo = repo.replace(/\.git\b/i, "");
const m = /^([^#\/:]+)\/([^#\/:]+)(\/([^#]+))?(#([^\/:]*))?$/.exec(repo);
if (!m)
return undefined;
const owner = m[1];
const project = m[2];
let fileName = m[4];
const tag = m[6];
const treeM = fileName && /^tree\/([^\/]+\/)/.exec(fileName);
if (treeM) {
// https://github.com/pelikhan/mono-demo/tree/master/demo2
fileName = fileName.slice(treeM[0].length);
// branch info?
}
return {
owner,
project,
slug: join(owner, project),
fullName: join(owner, project, fileName),
tag,
fileName,
};
}
async function fetchExtension(slug) {
const url = `https://cdn.makecode.com/api/gh/${slug}`;
const req = await (0, host_1.host)().requestAsync({
url
});
if (req.statusCode !== 200) {
error(`resolution of ${slug} failed (${req.statusCode})`);
(0, host_1.host)().exitWithStatus(1);
}
const script = JSON.parse(req.text);
return script;
}
async function searchCommand(query, opts) {
applyGlobalOptions(opts);
query = query.trim().toLowerCase();
msg(`searching for ${query}`);
const prj = await resolveProject(opts);
const targetid = prj.editor.targetJson.id;
const res = await (0, host_1.host)().requestAsync({
url: `${mkc_1.cloudRoot}ghsearch/${targetid}/${targetid}?q=${encodeURIComponent(query)}`
});
if (res.statusCode !== 200) {
error(`search request failed`);
(0, host_1.host)().exitWithStatus(1);
}
const payload = JSON.parse(res.text);
const { items } = payload;
items === null || items === void 0 ? void 0 : items.forEach(({ full_name, description, owner }) => {
msg(` ${full_name}`);
info(` https://github.com/${full_name}`);
if (description)
info(` ${description}`);
});
if (/jacdac/i.test(query)) {
const q = query.replace(/jacdac-*/i, "");
const exts = await jacdacMakeCodeExtensions();
for (const ext of exts.filter(ext => ext.client.name.indexOf(q) > -1 || ext.service.indexOf(q) > -1)) {
msg(` ${ext.client.name}`);
info(` https://${ext.client.repo}`);
}
}
}
exports.searchCommand = searchCommand;
async function stackCommand(opts) {
const srcmap = JSON.parse(await (0, host_1.host)().readFileAsync("built/binary.srcmap", "utf8"));
console.log((0, stackresolver_1.expandStackTrace)(srcmap, await (0, host_1.host)().readFileAsync(0, "utf8")));
}
exports.stackCommand = stackCommand;
async function addCommand(pkg, name, opts) {
applyGlobalOptions(opts);
opts.pxtModules = true;
msg(`adding ${pkg}`);
const prj = await resolveProject(opts);
await addDependency(prj, pkg, name);
prj.mainPkg = null;
await prj.maybeWritePxtModulesAsync();
}
exports.addCommand = addCommand;
async function addDependency(prj, pkg, name) {
var _a, _b;
pkg = pkg.toLowerCase().trim();
if (pkg === "jacdac")
pkg = "https://github.com/microsoft/pxt-jacdac";
else if (/^jacdac-/.test(pkg)) {
const exts = await jacdacMakeCodeExtensions();
const ext = exts.find(ext => ext.client.name === pkg);
if (ext) {
info(`found jacdac ${ext.client.repo}`);
pkg = ext.client.repo;
}
}
const rid = parseRepoId(pkg);
const pxtJson = await prj.readPxtConfig();
if (rid) {
const d = await fetchExtension(rid.slug);
const dname = name ||
join(rid.project, rid.fileName).replace(/^pxt-/, "").replace("/", "-");
pxtJson.dependencies[dname] = `github:${rid.fullName}#${d.version ? `v${d.version}` : d.defaultBranch}`;
info(`adding dependency ${dname}=${pxtJson.dependencies[dname]}`);
}
else {
const appTarget = await prj.service.languageService.getAppTargetAsync();
const bundledPkgs = (_b = (_a = appTarget === null || appTarget === void 0 ? void 0 : appTarget.bundleddirs) === null || _a === void 0 ? void 0 : _a.map((dir) => { var _a; return (_a = /^libs\/(.+)/.exec(dir)) === null || _a === void 0 ? void 0 : _a[1]; })) === null || _b === void 0 ? void 0 : _b.filter((dir) => !!dir);
const builtInPkg = bundledPkgs === null || bundledPkgs === void 0 ? void 0 : bundledPkgs.find(dir => dir === pkg);
if (!builtInPkg) {
const possiblyMeant = bundledPkgs === null || bundledPkgs === void 0 ? void 0 : bundledPkgs.filter(el => (el === null || el === void 0 ? void 0 : el.toLowerCase().indexOf(pkg)) !== -1);
if (possiblyMeant === null || possiblyMeant === void 0 ? void 0 : possiblyMeant.length) {
error(`Did you mean ${possiblyMeant === null || possiblyMeant === void 0 ? void 0 : possiblyMeant.join(", ")}?`);
}
else {
error("unknown package, try https://github.com/.../... for github extensions");
}
(0, host_1.host)().exitWithStatus(1);
}
const collidingHwVariant = Object.keys(pxtJson.dependencies)
.find(dep => dep.toLowerCase().replace(/---.+$/, "") === pkg.replace(/---.+$/, "")
&& pxtJson.dependencies[dep] === "*");
if (collidingHwVariant) {
delete pxtJson.dependencies[collidingHwVariant];
}
pxtJson.dependencies[builtInPkg] = "*";
info(`adding builtin dependency ${builtInPkg}=*`);
}
await (0, host_1.host)().writeFileAsync("pxt.json", JSON.stringify(pxtJson, null, 4), "utf8");
}
function isKV(v) {
return !!v && typeof v === "object" && !Array.isArray(v);
}
function jsonMergeFrom(trg, src) {
if (!src)
return;
Object.keys(src).forEach(k => {
if (isKV(trg[k]) && isKV(src[k]))
jsonMergeFrom(trg[k], src[k]);
else if (Array.isArray(trg[k]) && Array.isArray(src[k]))
trg[k] = trg[k].concat(src[k]);
else
trg[k] = src[k];
});
}
async function readCfgAsync(cfgpath, quiet = false) {
const files = [];
return readCfgRec(cfgpath);
async function readCfgRec(cfgpath) {
if (files.indexOf(cfgpath) >= 0) {
error(`Config file loop: ${files.join(" -> ")} -> ${cfgpath}`);
(0, host_1.host)().exitWithStatus(1);
}
const cfg = await cfgFile(cfgpath);
const currCfg = {};
files.push(cfgpath);
for (const fn of cfg.include || []) {
const resolved = path.resolve(path.dirname(cfgpath), fn);
if (!quiet)
info(` include: ${resolved}`);
jsonMergeFrom(currCfg, readCfgRec(resolved));
}
jsonMergeFrom(currCfg, cfg);
delete currCfg.include;
files.pop();
return currCfg;
async function cfgFile(cfgpath) {
let cfg;
try {
cfg = JSON.parse(await (0, host_1.host)().readFileAsync(cfgpath, "utf8"));
}
catch (e) {
error(`Can't read config file: '${cfgpath}'; ` + e.message);
(0, host_1.host)().exitWithStatus(1);
}
const lnk = cfg.links;
if (lnk) {
const mkcFolder = path.resolve(".", path.dirname(cfgpath));
for (const k of Object.keys(lnk)) {
lnk[k] = path.resolve(mkcFolder, lnk[k]);
}
}
return cfg;
}
}
}
function concatUint8Arrays(bufs) {
let size = 0;
for (const buf of bufs) {
size += buf.length;
}
const res = new Uint8Array(size);
let offset = 0;
for (const buf of bufs) {
res.set(buf, offset);
offset += buf.length;
}
return res;
}
async function shareCommand(opts) {
const shareLink = await (0, share_1.shareProjectAsync)(opts);
if (shareLink) {
info(`Success! Project shared to ${shareLink}`);
}
else {
error("Unable to share project");
}
}
exports.shareCommand = shareCommand;
async function getSimHTML(opts) {
return getExpandedHTML(opts, "cachedSimulatorKey", "sim");
}
exports.getSimHTML = getSimHTML;
async function getWebConfig(opts) {
applyGlobalOptions(opts);
const project = await resolveProject(opts);
return project.editor.webConfig;
}
exports.getWebConfig = getWebConfig;
async function getAssetEditorHTML(opts) {
return getExpandedHTML(opts, "cachedAssetEditorKey", "asseteditor");
}
exports.getAssetEditorHTML = getAssetEditorHTML;
async function getExpandedHTML(opts, downloadInfoKey, pageName) {
applyGlobalOptions(opts);
const project = await resolveProject(opts);
const cache = await project.getCacheAsync();
const infoKey = project.editor.website + "-info";
const rawDownloadInfo = await cache.getAsync(infoKey);
const parsed = (rawDownloadInfo ? JSON.parse((0, host_1.host)().bufferToString(rawDownloadInfo)) : {});
if (parsed[downloadInfoKey]) {
const existing = await cache.getAsync(parsed[downloadInfoKey]);
if (existing) {
return (0, host_1.host)().bufferToString(existing);
}
}
const key = `${project.editor.website}-${pageName}.html`;
const expanded = await getExpandedPageFromCache(key, cache, project.editor.website, project.editor.cdnUrl);
const expandedKey = `${project.editor.website}-${pageName}Expanded.html`;
await cache.setAsync(expandedKey, (0, host_1.host)().stringToBuffer(expanded));
// save that this page has been generated. this key is cleared when the cache is updated
parsed[downloadInfoKey] = expandedKey;
await cache.setAsync(infoKey, (0, host_1.host)().stringToBuffer(JSON.stringify(parsed)));
return expanded;
}
async function getExpandedPageFromCache(cacheKey, cache, website, cdnUrl) {
let pageHTML = (0, host_1.host)().bufferToString(await cache.getAsync(cacheKey));
const imageUrls = [];
pageHTML = pageHTML.replace(/https:\/\/[\w\/\.\-]+/g, f => {
if (f.startsWith(cdnUrl) && f.endsWith(".png")) {
imageUrls.push(f);
}
return f;
});
for (const url of imageUrls) {
const resp = await downloader.requestAsync({ url });
if (resp.statusCode !== 200 || !resp.buffer)
continue;
const encoded = await (0, host_1.host)().base64EncodeBufferAsync(resp.buffer);
const dataUri = `data:image/png;base64,${encoded}`;
pageHTML = pageHTML.replace(url, dataUri);
}
const dom = new xmldom_1.DOMParser().parseFromString(pageHTML, "text/html");
const scripts = [];
const toRemove = [];
const processElement = async (element, srcAttribute, fileExtension) => {
if (!element.hasAttribute(srcAttribute))
return undefined;
const srcPath = element.getAttribute(srcAttribute);
if (!srcPath.endsWith(fileExtension))
return undefined;
let filename = srcPath;
if (srcPath.startsWith(website)) {
filename = srcPath.slice(website.length);
}
if (filename.indexOf("/") !== -1) {
filename = filename.split("/").pop();
}
else {
filename = filename.split("-").pop();
}
const key = website + "-" + filename;
const contents = await cache.getAsync(key);
if (!contents)
return undefined;
let contentString = (0, host_1.host)().bufferToString(contents);
return contentString.trim();
};
for (const element of dom.getElementsByTagName("script")) {
const contentString = await processElement(element, "src", ".js");
if (contentString) {
scripts.push(encodeURIComponent(contentString));
}
else {
scripts.push(encodeURIComponent(element.textContent));
}
toRemove.push(element);
}
for (const element of dom.getElementsByTagName("link")) {
const contentString = await processElement(element, "href", ".css");
if (contentString) {
const newStyle = dom.createElement("style");
newStyle.textContent = `\n${contentString}\n`;
element.parentElement.insertBefore(newStyle, element);
toRemove.push(element);
}
}
for (const element of toRemove) {
element.parentElement.removeChild(element);
}
const metaScript = `
const allScripts = [\`${scripts.join("`,`")}\`];
for (const scriptEntry of allScripts) {
const el = document.createElement("script");
el.textContent = decodeURIComponent(scriptEntry);
document.head.appendChild(el);
}
`;
const scriptElement = dom.createElement("script");
scriptElement.textContent = metaScript;
dom.getElementsByTagName("head").item(0).appendChild(scriptElement);
return new xmldom_1.XMLSerializer().serializeToString(dom);
}
//# sourceMappingURL=commands.js.map