UNPKG

elm-basic-compile

Version:

elm-compiler built with GHCJS and wrapped in a simple interface

674 lines (636 loc) 23.6 kB
var q = require('q'); var XMLHttpRequest = XMLHttpRequest || require('xmlhttprequest').XMLHttpRequest; var semver = require("semver"); var ls = require("logic-solver"); var btoa = btoa || require('btoa'); var pv = require("./package-version"); var elmStatic = require("./elm-static"); var compiler = require("./elm-compiler-interface"); if (typeof localStorage === "undefined" || localStorage === null) { var LocalStorage = require('node-localstorage').LocalStorage; localStorage = new LocalStorage('./elm-cache'); } var defaultImports = [ ["Basics"], ["Debug"], ["List"], ["Maybe"], ["Result"], ["String"], ["Tuple"], ["Platform"], ["Platform","Cmd"], ["Platform","Sub"] ]; /* * user,project: github.com/<user>/<project> * var projectSpec = * { user: "prozacchiwawa", * project: "effmodel", * version: "2.0.1" * }; * retriever.retrieveJson(projectSpec) -> * In one implementation, a promise to retrieve * "https://raw.githubusercontent.com/prozacchiwawa/effmodel/2.0.1/elm-package.json" * retriever.retrieveSource(projectSpec,"src",["EffModel"],"elm") -> * In one implementation, a promise to retrieve * "https://raw.githubusercontent.com/prozacchiwawa/effmodel/2.0.1/src/EffModel.elm" * Could be a promise for local storage cache, file read etc. */ function ElmPackage(retriever,projectSpec,solver) { this.solver = solver || new PackageSolver(retriever); this.projectSpec = projectSpec; this.retriever = retriever; this.elmPackage = null; this.exactDeps = null; this.packageDeps = {}; this.importsRe = /^\/\/[ ]*import[ ]+([^\/\n\r]*)\/\//m; this.wsRe = /^\s+|\s+$/g; var self = this; this.solver.have = this.solver.have || {}; var pname = pv.packageNameString(projectSpec); this.solver.have[pname] = this; this.solver.deps = this.solver.deps || {}; this.compiler = null; this._dolog = false; this.log = function() { if (this._dolog) { var args = [].slice.call(arguments); console.log.apply(console, args); } }; this.afterElmPackage = compiler.init().then(function(comp) { self.compiler = comp; }).then(function() { return retriever.retrieveJson(projectSpec); }).then(function(pspec) { return JSON.parse(pspec); }).then(function(pspec) { self.solver.injectPackage(projectSpec,pspec); return self.solver.fillTree(projectSpec).then(function() { return pspec; }); }).then(function(pspec) { var solution = self.solver.solve(projectSpec); self.exactDeps = solution; return pspec; }).then(function(pspec) { self.elmPackage = pspec; return self.elmPackage; }); } ElmPackage.prototype.debug = function(on) { this._dolog = on; } /* * return a promise for a pair of elm source file or null, * javascript source file or null * * If neither exists, reject with a message. * If either gives an error other than 404, reject with a message. */ ElmPackage.prototype.findSourceFiles = function(modname) { function nullOn404(err) { if (err.status == 404) { return null; } else { throw err; } } var self = this; function tryOneSourceDir(sourceDir,modname,ext) { return self.retriever.retrieveSource( self.projectSpec, sourceDir, modname, ext). fail(nullOn404); } function tryAllSourceDirs(results,sourceDirs,modname) { for (var i = 0; i < sourceDirs.length; i++) { var sourceDir = sourceDirs[i]; if (modname[0] == "Native") { results.push(tryOneSourceDir(sourceDir,modname,"js"). then(function(file) { return ["js",file]; })); } else { results.push(tryOneSourceDir(sourceDir,modname,"elm"). then(function(file) { return ["elm",file]; })); } } } var promise = null; function createPromiseForFiles(modname) { var results = []; tryAllSourceDirs( results, self.elmPackage["source-directories"], modname); return q.all(results); } if (this.elmPackage == null) { this.afterElmPackage = this.afterElmPackage.then(function() { return createPromiseForFiles(modname); }); return this.afterElmPackage; } else { return createPromiseForFiles(modname); } } ElmPackage.prototype._collectPkgDeps = function(downstreamPackages) { var name = pv.packageNameString(self.productName); downstreamPackages.push(name); for (var k in self.packageDeps) { self.packageDeps[k]._collectPkgDeps(downstreamPackages); } } ElmPackage.prototype.expandPackage = function(extra) { var self = this; if (this.elmPackage == null) { this.afterElmPackage = this.afterElmPackage.then(function() { return self.expandPackage(extra); }); return this.afterElmPackage; } var packageName = pv.packageNameString(this.projectSpec); var exposed = this.elmPackage["exposed-modules"].map(function(m) { return m; }); if (extra) { exposed.push.apply(exposed,extra); } self.log("/* expandPackage",self.projectSpec,exposed,"*/"); // Put in exposed modules from imports self.solver.have = self.solver.have || {}; var downstreamPackages = Object.keys(self.elmPackage.dependencies); return self._expandDep(downstreamPackages,0).then(function() { downstreamPackages.map(function(k) { var ver = self.exactDeps[k]; var specAr = k.split("/"); var modspec = {user: specAr[0], project: specAr[1], version: ver}; self.packageDeps[k] = self.solver.have[k]; }); }); } ElmPackage.prototype._findPackageForModule = function(m) { var self = this; var split = m.split(':'); if (split.length > 1) { return self.solver.have[split[1]]; } m = split[0]; for (var p in self.solver.have) { var pkg = self.solver.have[p]; if (pkg.elmPackage["exposed-modules"].indexOf(m) != -1) { return pkg; } } return self; } ElmPackage.prototype._compileIfNeeded = function(mods,i) { var self = this; if (i >= mods.length) { self.log("/* Done compiling */"); return q.fcall(function() { }); } var m = mods[i]; if (typeof(m) !== 'string') { m = m.join('.'); } var who = self._findPackageForModule(m); var promise = null; var split = m.split(':'); if (split.length > 1) { m = split[0]; } self.log("/* _compileIfNeeded",who.projectSpec,m,"*/"); var pkgname = pv.packageNameString(who.projectSpec); var s = {name: m, elm: null, js: null, imports: null, pkg: who, exported: who.elmPackage["exposed-modules"].indexOf(pkgname) != -1}; who.solver.deps[m] = who.solver.deps[m] || {}; who.solver.deps[m][pkgname] = who.solver.deps[m][pkgname] || s; self.log("/* deps",Object.keys(who.solver.deps[m]),"*/"); s = who._getModule(m); self.log("/* _compileIfNeeded",s,"*/"); if (!s.elm && !s.js) { self.log("/* Get source for",m,"*/"); return who.findSourceFiles(m.split(".")).then(function(sres) { var res = sres[0]; self.log("/* Got source for",m,res[0],"*/"); s[res[0]] = res[1]; if (s.js) { var jsImport = s.js.match(self.importsRe); if (jsImport != null) { var importsStrip = jsImport[1].replace(self.wsRe, ''); s.imports = importsStrip.split(',').map(function(i) { return i.replace(self.wsRe, ''); }).filter(function(imp) { return imp !== ''; }); } } else if (s.elm) { if (!self.solver.deps[m].uncached) { var intf = localStorage.getItem(m + ":interface"); var elmo = localStorage.getItem(m + ":elmo"); s.intf = intf; s.elmo = elmo; } } self.log("/* Src */"); }).fail(function(e) { console.error(e); throw e; }).then(function() { self.log("/* New compile cycle */"); return self._compileIfNeeded(mods,i); }); } else if (!s.imports) { self.log("/* Parse",s.name,"*/"); return who.compiler.parse([who.projectSpec.user,who.projectSpec.project], s.elm).then(function(res) { self.log("/* Got Imports",s.pkg.projectSpec,m,res,i,"*/"); if (res.length == 1) { throw new Error(res[0].join("\n")); } var pkgname = pv.packageNameString(who.projectSpec); var importsIn = res[1].map(function(mod) { return mod.join("."); }); var imports = []; for (var j = 0; j < importsIn.length; j++) { var k = importsIn[j]; var keys = who.solver.deps[k] ? Object.keys(who.solver.deps[k]) : []; self.log("/* import?",k,keys,"*/"); if (keys.length > 1) { for (var l = 0; l < keys.length; l++) { imports.push(k+':'+pkgname); } } else { imports.push(k); } } s.imports = imports; return who._compileIfNeeded(s.imports,0); }).then(function() { return self._compileIfNeeded(mods,i); }); } else if (s.elm && !s.intf) { self.log("/* Interface",m,"*/"); var who = self._findPackageForModule(m); var needModules = s.imports.filter(function(k) { return !who._getModule(k) && k.split('.')[0] !== 'Native'; }); if (needModules.length > 0) { var expandedModules = []; self.log("/* Unretrieved mods",needModules,"*/"); for (var j = 0; j < needModules.length; j++) { var mod = needModules[j]; for (var k in self.solver.deps[mod]) { expandedModules.push(mod+':'+k); } } return who._compileIfNeeded(expandedModules,0).then(function() { self.log("/* Continue compilation */"); return self._compileIfNeeded(mods,i); }); } var compileInputs = []; var ifacesMod = Object.keys(who.solver.deps); var ifaces = []; for (var j = 0; j < ifacesMod.length; j++) { var k = ifacesMod[j]; var keys = Object.keys(who.solver.deps[k]); if (keys.length > 1) { for (var l = 0; l < keys.length; l++) { ifaces.push(k+':'+keys[l]); } } else if (keys.length == 1) { ifaces.push(k); } } ifaces = ifaces.filter(function(k) { return k.split('.')[0] !== 'Native' && who._getModule(k).intf; }); for (var j = 0; j < ifaces.length; j++) { var k = ifaces[j]; for (var p in who.solver.deps[k]) { var ss = who.solver.deps[k][p]; var whoj = ss.pkg; var spkg = [whoj.projectSpec.user,whoj.projectSpec.project]; self.log("/* Import",k,"in",spkg,"*/"); compileInputs.push([ [[spkg,k.split(".")],whoj.projectSpec.version], ss.intf ]); } } var name = [who.projectSpec.user, who.projectSpec.project]; var exposed = who.elmPackage["exposed-modules"]; var isExposed = exposed.indexOf(m) != -1 ? "true" : "false"; var source = self._getModule(m).elm; /* var intf = localStorage.getItem(m + ":interface"); var elmo = localStorage.getItem(m + ":elmo"); if (intf && elmo) { self._getModule(m).intf = intf; self._getModule(m).elmo = elmo; return self._compileIfNeeded(mods,i); } */ self.log("/* Compile",m,isExposed,JSON.stringify(compileInputs.map(function(i) { return i[0]; })),"*/"); return self.compiler.compile(name,isExposed,source,compileInputs).fail(function(error) { console.error("compiling",name,error); throw error; }).then(function(res) { if (res[0] !== "false") { var ss = self._getModule(m); if (!self.solver.deps[m].uncached) { localStorage.setItem(m + ":interface", res[1]); localStorage.setItem(m + ":elmo", res[2]); } self._getModule(m).intf = res[1]; self._getModule(m).elmo = res[2]; } else { throw new Error(res[1].join("\n")); } return [res[0] !== "false", res[1], res[2]]; }).then(function() { return self._compileIfNeeded(mods,i); }); } else { self.log("/* Mod advance",i,mods[i],mods,"*/"); return self._compileIfNeeded(mods,i+1); } } ElmPackage.prototype.compileModule = function(modname,fresh) { if (this.elmPackage == null) { this.afterElmPackage = this.afterElmPackage.then(function() { return this.compileModule(modname,fresh); }); return this.afterElmPackage; } if (fresh) { this.solver.deps[modname] = this.solver.modname || {}; this.solver.deps[modname].uncached = true; } var who = this._findPackageForModule(modname); return who._compileIfNeeded([modname],0); } ElmPackage.prototype._collect = function(compileOrder,m) { var self = this; if (compileOrder[m]) { return; } var who = self._findPackageForModule(m); compileOrder[m] = compileOrder[m] || {}; var ideps = self._getModule(m); var imports = ideps.imports || []; for (var i = 0; i < imports.length; i++) { var k = imports[i]; compileOrder[m][k] = true; who._collect(compileOrder,k); } } ElmPackage.prototype._getModule = function(m) { var self = this; var pkgname = pv.packageNameString(self.projectSpec); var split = m.split(':'); if (split.length > 1) { pkgname = split[1]; m = split[0]; } if (self.solver.deps[m]) { var packageChoices = Object.keys(self.solver.deps[m]); if (packageChoices.length === 0) { return null; } else if (packageChoices.length === 1) { return self.solver.deps[m][packageChoices[0]]; } else { var primaryChoice = packageChoices.filter(function(p) { return self.solver.deps[m][p].exported; }); if (primaryChoice.length > 0) { return self.solver.deps[m][primaryChoice[0]]; } var closeChoice = packageChoices.filter(function(p) { return p === pkgname; }); if (closeChoice.length > 0) { return self.solver.deps[m][closeChoice[0]]; } return self.solver.deps[m][packageChoices[0]]; } } else { return null; } } ElmPackage.prototype._linkOne = function(compileOrder,result) { var self = this; for (var k in compileOrder) { var o = Object.keys(compileOrder[k]).filter(function(m) { return !!compileOrder[m]; }); self.log("/* remaining to link for",k,":",o,"*/"); if (o.length != 0) { continue; } var m = k; var modname = m.split("."); var js = null; var who = self._findPackageForModule(m); if (modname[0] == "Native") { var s = who._getModule(m); if (!s || !s.js) { throw new Error("Could not find js source for"+JSON.stringify(who.projectSpec)+":"+m); } js = who._getModule(m).js; } else { if (!who._getModule(m).elmo) { self.log("/* Make ELMO",m,"*/"); return who.compileModule(m).then(function() { return self._linkOne(compileOrder,result); }); } js = who._getModule(m).elmo; } result.push("//***"+modname+" "+JSON.stringify(who.projectSpec)); result.push(js); delete compileOrder[m]; return self._linkOne(compileOrder,result); } return q.fcall(function() { return result; }); } ElmPackage.prototype._expandDep = function(mods,i) { var self = this; if (i >= mods.length) { return q.fcall(function() { }); } var m = mods[i]; self.log("/* _expandDep",m,"*/"); if (!self.solver.have[m]) { var ver = self.exactDeps[m]; var specAr = m.split("/"); var modspec = {user: specAr[0], project: specAr[1], version: ver}; var pkg = new ElmPackage(self.retriever,modspec,self.solver); self.solver.have[m] = pkg; } return self.solver.have[m].expandPackage().then(function() { return self._expandDep(mods,i+1); }); } ElmPackage.prototype.link = function(mods) { var self = this; if (this.elmPackage == null) { this.afterElmPackage = this.afterElmPackage.then(function() { return self.link(mods); }); return this.afterElmPackage; } var result = []; var compileOrderKeys = Object.keys(self.solver.deps); if (mods) { compileOrderKeys.push.apply(compileOrderKeys,mods); } var compileOrder = {}; for (var i = 0; i < compileOrderKeys.length; i++) { var k = compileOrderKeys[i]; self._collect(compileOrder, k); } self.log("/*Link:",compileOrder,"*/"); return self._expandDep(Object.keys(self.packageDeps),0).then(function() { return self._linkOne(compileOrder, result); }).then(function(result) { var fin = [elmStatic.prelude.join("\n")]; fin.push.apply(fin,result); fin.push(elmStatic.footer(self.projectSpec).join("\n")); fin = fin.join("\n"); return fin; }); } /* * A solver for package versions using semver. * It contains a map of package names to version lists, and another * map of package names to version constraint lists. * Each time the user adds a package, any packages that have unmet * constraints are removed. * * solver.getBestDeps() will retrieve a list of packageSpec that meets * the criteria or null. * * retriever is an object that can fetch new package version lists and * elm-package.json. */ function PackageSolver(retriever) { this.retriever = retriever; this.solver = new ls.Solver(); this.buildSolution = null; this.versions = {}; } PackageSolver.prototype._makeDepsPromise = function(pname) { var self = this; return function(ver) { self._promiseDeps(self.versions[pname][ver]); } } /* * Manually create a package, given a packageSpec and an elm-package json. */ PackageSolver.prototype.injectPackage = function(packageSpec, json) { var pname = pv.packageNameString(packageSpec); this.versions[pname] = this.versions[pname] || {}; this.versions[pname][json.version] = json; } /* * Promise a filled dep tree for a specific package. */ PackageSolver.prototype.fillTree = function(packageSpec) { var simpleName = pv.packageNameString(packageSpec); var vset = Object.keys(this.versions[simpleName]); var self = this; return q.all(vset.map(function(v) { return self._promiseDeps(self.versions[simpleName][v]); })); } /* Promise all dependencies to be retrieved. */ PackageSolver.prototype._promiseDeps = function(json) { var self = this; var depkeys = Object.keys(json.dependencies); var wantdeps = depkeys.filter(function(k) { return !self.versions[k]; }); return q.all(wantdeps.map(function(k) { var reqspec = pv.parsePackageName(k); return self.retriever.retrieveTags(reqspec).then(function(tags) { return q.all(tags.map(function(v) { var pspec = pv.parsePackageName(k); pspec.version = v; return self.retriever.retrieveJson(pspec).then(function(j) { var json = JSON.parse(j); self.versions[k] = self.versions[k] || {}; self.versions[k][v] = json; return json; }); })); }).then(function() { var pspec = pv.parsePackageName(k); return self.fillTree(pspec); }); })); } PackageSolver.prototype._runPackage = function(usedPackages,pkg) { var solver = this.solver; var captured = this.versions; var packageName = pv.packageNameString(pkg); var versions = Object.keys(captured[packageName]); for (var j = 0; j < versions.length; j++) { var v = versions[j]; var fullPackageName = packageName + ":" + v; if (usedPackages[fullPackageName]) { continue; } usedPackages[fullPackageName] = true; var json = captured[packageName][v]; var deps = json.dependencies; var depkeys = Object.keys(deps); for (var l = 0; l < depkeys.length; l++) { var dep = depkeys[l]; var depexp = deps[dep]; var allowedVersions = pv.getAllowedVersionsOfPackage(captured, dep, depexp); var disallowedVersions = pv.getNotAllowedVersionsOfPackage(captured, dep, allowedVersions); var diskeys = disallowedVersions.map(function(v) { return dep + ":" + v; }); for (var i = 0; i < diskeys.length; i++) { solver.require(ls.implies(diskeys[i], ls.not(fullPackageName))); } var verkeys = allowedVersions.map(function(v) { return dep + ":" + v; }); solver.require(ls.implies(fullPackageName, ls.or.apply(ls, verkeys))); for (var m = 0; m < allowedVersions.length; m++) { var up = dep.split("/"); var allow = allowedVersions[m]; var usePspec = { user: up[0], project: up[1], version: allow }; this._runPackage(usedPackages,usePspec); } } } } PackageSolver.prototype.solve = function(pspec) { var expr = {_:""}; var captured = this.versions; var packageName = pv.packageNameString(pspec) + ":" + pspec.version; this.solver.require(packageName); this._runPackage(expr, pspec); var solution = this.solver.solve(); if (!solution) { this.buildSolution = null; return null; } var deps = solution.getTrueVars(); var res = {}; for (var i = 0; i < deps.length; i++) { var s = deps[i].split(':'); res[s[0]] = s[1]; } this.buildSolution = res; return res; } /* * The haskell code can tell us what elmi files will be required to compile * a specific module given a set of exact dependencies, which have been * created by PackageSolver::solve. We must however build to elmi. * * Once elmo files are generated, we link by concatenating javascript from * the data store. */ module.exports.ElmPackage = ElmPackage; module.exports.PackageSolver = PackageSolver;