Created
May 1, 2016 03:13
-
-
Save rksm/69df8e50b174aa6d7f93131293c9b4af to your computer and use it in GitHub Desktop.
lively.vm es6 + cjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*global require, before, after, beforeEach, afterEach, describe, it*/ | |
import { expect } from "mocha-es6"; | |
import { fork } from "child_process" | |
if (System.get("@system-env").node) { | |
describe("bootstrap", function() { | |
this.timeout(5000); | |
it("vm in node", () => | |
new Promise(function(resolve, reject) { | |
var proc = fork("bootstrap-nodejs.js", {cwd: "..", silent: true}), | |
out = ""; | |
proc.on("error", reject); | |
proc.on("exit", function() { resolve(out); }); | |
proc.stdout.on("data", d => out += String(d)); | |
}) | |
.then(out => expect(out).to.match(/\nDONE\n/m))) | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*global before, after, beforeEach, afterEach, describe, it*/ | |
import { expect } from "mocha-es6"; | |
import lang from "lively.lang"; | |
import { es6, cjs } from "lively.vm"; | |
import { Module } from "module"; | |
// yet-another-cjs-module depends on another-cjs-module | |
// another-cjs-module depends on some-cjs-module | |
var module1, module2, module3, module1Full, module2Full, module3Full, parentModule; | |
var __filename = cjs.resolve("./cjs-test.js"); | |
function require(id) { | |
return Module._load(id, {id: __filename}) | |
} | |
if (System.get("@system-env").node) { | |
describe("common-js modules", () => { | |
before(() => { | |
cjs.wrapModuleLoad(); | |
module1 = "./test-resources/cjs/module1"; | |
module2 = "./test-resources/cjs/module2"; | |
module3 = "./test-resources/cjs/module3"; | |
module1Full = cjs.resolve(module1, __filename); | |
module2Full = cjs.resolve(module2, __filename); | |
module3Full = cjs.resolve(module3, __filename); | |
parentModule = __filename; | |
}); | |
after(() => cjs.unwrapModuleLoad()); | |
beforeEach(() => require(module1)); | |
afterEach(() => cjs.forgetModule(module1)); | |
describe("module state", () => { | |
it("captures internal module state", () => { | |
expect(cjs.envFor(module1)) | |
.deep.property('recorder.internalState').equals(23); | |
expect(cjs.envFor(module1)) | |
.deep.property('recorder.exports.state').equals(42); | |
}); | |
}); | |
describe("eval", () => { | |
it("evaluates inside of module", () => | |
cjs.runEval("internalState", {targetModule: module1, parentModule: parentModule}) | |
.then(evalResult => expect(evalResult).property("value").equals(23))); | |
}); | |
describe("reloading", () => { | |
beforeEach(() => require(module3)); | |
afterEach(() => cjs.forgetModule(module3)); | |
it("computes required modules of some module", () => { | |
expect(cjs.findRequirementsOf(module3)).to.deep.equal( | |
[cjs.resolve(module2Full), cjs.resolve(module1Full), "fs"]); | |
}); | |
it("computes dependent modules of some module", () => { | |
expect(cjs.findDependentsOf(module1)).to.deep.equal( | |
[parentModule, cjs.resolve(module2Full), cjs.resolve(module3Full)]); | |
}); | |
it("can reload module dependencies", () => { | |
expect(require(module3).myVal).to.equal(44); | |
// we change module1 and check that the value of module3 that indirectly | |
// depends on module1 has changed as well | |
return cjs.sourceOf(module1, parentModule) | |
.then(s => s.replace(/(externalState = )([0-9]+)/, "$123")) | |
.then(s => cjs.runEval(s, {targetModule: module1, parentModule: parentModule})) | |
.then(() => cjs.forgetModuleDeps(module1, parentModule)) | |
.then(() => expect(require(module3).myVal).to.equal(25)) | |
}); | |
}); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*global process, require, global, __dirname*/ | |
var isNode = System.get("@system-env").node | |
var GLOBAL = typeof window !== "undefined" ? window : (typeof Global !== "undefined" ? Global : global); | |
var debug = false; | |
// var debug = true; | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// utils | |
import * as path from "path"; | |
import * as lang from "lively.lang"; | |
var join = lang.string.joinPath, | |
isAbsolute = p => !!p.match(/^(\/|[^\/]+:\/\/)/), | |
relative = path.relative || ((base, path) => path), | |
dirname = path => path.replace(/\/[^\/]+$/, ""); | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// node interface | |
import { Module } from "module"; | |
var nodeModules = isNode ? { | |
get cache() { return Module._cache; }, | |
get require() { return (name, parent) => System._nodeRequire(name, parent); }, | |
get resolve() { return (name, parent) => Module._resolveFilename(name, parent); }, | |
} : { | |
get cache() { | |
console.warn("[lively.vm cjs] module cache accessor used on non-node system"); | |
return {}; | |
}, | |
get require() { | |
return (name, parent) => { | |
console.warn("[lively.vm cjs] require used on non-node system"); | |
return undefined; | |
}; | |
}, | |
get resolve() { | |
return (name, parent) => { | |
console.warn("[lively.vm cjs] resolveFilename used on non-node system"); | |
return name; | |
}; | |
} | |
} | |
var __dirname = System.normalizeSync("lively.vm/lib/").replace(/^[^:]+:\/\//, ''); | |
var __filename = isNode ? | |
join(__dirname, "commonjs-interface.js") : | |
System.normalizeSync("lively.vm/lib/commonjs-interface.js").replace(/^[^:]+:\/\//, ''); | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// other deps | |
import * as fs from "fs"; | |
import * as evaluator from "./evaluator"; | |
function callsite(){ | |
var orig = Error.prepareStackTrace; | |
Error.prepareStackTrace = function(_, stack){ return stack; }; | |
var err = new Error; | |
Error.captureStackTrace(err, arguments.callee); | |
var stack = err.stack; | |
Error.prepareStackTrace = orig; | |
return stack; | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// helper | |
function resolve(file, parent) { | |
if (typeof parent === "string") parent = {id: parent}; | |
// normal resolve | |
try { | |
return nodeModules.resolve(file, parent); | |
} catch (e) {} | |
// manual lookup using path | |
if (!isAbsolute(file) && parent) { | |
if (!file.match(/\.js$/)) file += ".js"; | |
try { | |
return nodeModules.resolve(join(parent.id, "..", file)); | |
} catch (e) {} | |
} | |
// manual lookup using callstack | |
var frames = callsite(), frame; | |
for (var i = 2; i < frames.length; i++) { | |
frame = frames[i]; | |
var frameFile = frame.getFileName(); | |
if (!frameFile) continue; | |
var dir = dirname(frameFile), | |
full = join(dir, file); | |
try { return nodeModules.resolve(full, parent); } catch (e) {} | |
} | |
// last resort: current working directory | |
try { | |
return nodeModules.resolve(join(process.cwd(), file), parent); | |
} catch (e) {} | |
return file; | |
} | |
function sourceOf(moduleName, parent) { | |
var read = lang.promise.convertCallbackFun(fs.readFile); | |
return read(resolve(moduleName, parent)).then(String); | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// module wrapping + loading | |
// maps filenames to envs = {isLoaded: BOOL, loadError: ERROR, recorder: OBJECT} | |
var loadedModules = {}, | |
requireMap = {}, | |
originalCompile = null, originalLoad = null, | |
// exceptions = [ | |
// // (id) => console.log(!!id.match(/lib\/commonjs-interface\.js$/)) || id.match(/lib\/commonjs-interface\.js$/), | |
// (id) => id.indexOf("commonjs-interface.js") > -1, | |
// (id) => ["lively.lang/node_modules", "lively.vm/node_modules", "code-pump/node_modules"] | |
// .some(ignore => id.indexOf(ignore) > -1) | |
// ], | |
// exceptions = [module.filename], | |
exceptions = [], | |
scratchModule = join(__dirname, "commonjs-scratch.js"); // fallback eval target | |
function getExceptions() { return exceptions; } | |
function setExceptions(value) { return exceptions = value; } | |
function instrumentedFiles() { return Object.keys(loadedModules); } | |
function isLoaded(fileName) { return nodeModules.cache[fileName] && fileName in loadedModules; } | |
function ensureRecorder(fullName) { return ensureEnv(fullName).recorder; } | |
function ensureEnv(fullName) { | |
return loadedModules[fullName] | |
|| (loadedModules[fullName] = { | |
isInstrumented: false, | |
loadError: undefined, | |
// recorderName: "eval_rec_" + path.basename(fullName).replace(/[^a-z]/gi, "_"), | |
// recorderName: "eval_rec_" + fullName.replace(/[^a-z]/gi, "_"), | |
recorderName: "__lv_rec__", | |
recorder: Object.create(GLOBAL) | |
}); | |
} | |
function prepareCodeForCustomCompile(source, filename, env) { | |
source = String(source); | |
var magicVars = ["exports", "require", "module", "__filename", "__dirname"], | |
tfmOptions = { | |
topLevelVarRecorder: env.recorder, | |
varRecorderName: env.recorderName, | |
dontTransform: [env.recorderName, "global"].concat(magicVars), | |
recordGlobals: true | |
}, | |
header = `var __cjs = System.get('file://${__filename}'),\n ${env.recorderName} = __cjs.envFor('${filename}').recorder;\n` | |
+ magicVars.map(varName => `${env.recorderName}.${varName} = ${varName};`).join("\n"); | |
try { | |
return (header + "\n" | |
+ ";(function() {\n" | |
+ evaluator.evalCodeTransform(source, tfmOptions) | |
+ "\n})();"); | |
} catch (e) { return e; } | |
} | |
var loadDepth = 0; | |
function customCompile(content, filename) { | |
// wraps Module.prototype._compile to capture top-level module definitions | |
if (exceptions.some(exc => exc(filename)) || isLoaded(filename)) | |
return originalCompile.call(this, content, filename); | |
debug && console.log("[lively.vm customCompile] %s", filename); | |
// console.log(lang.string.indent("[lively.vm commonjs] loads %s", " ", loadDepth), filename); | |
// if cache was cleared also reset our recorded state | |
if (!nodeModules.cache[filename] && loadedModules[filename]) | |
delete loadedModules[filename]; | |
var env = ensureEnv(filename), | |
_ = env.isInstrumented = true, | |
tfmedContent = prepareCodeForCustomCompile(content, filename, env); | |
if (tfmedContent instanceof Error) { | |
console.warn("Cannot compile module %s:", filename, tfmedContent); | |
env.loadError = tfmedContent; | |
var result = originalCompile.call(this, content, filename); | |
return result; | |
} | |
GLOBAL[env.recorderName] = env.recorder; | |
loadDepth++; | |
try { | |
var result = originalCompile.call(this, tfmedContent, filename); | |
env.loadError = undefined; | |
return result; | |
} catch (e) { | |
console.log("-=-=-=-=-=-=-=-"); | |
console.error("[lively.vm commonjs] evaluator error loading module: ", e.stack || e); | |
console.log("-=-=-=-=-=-=-=-"); | |
console.log(tfmedContent); | |
console.log("-=-=-=-=-=-=-=-"); | |
env.loadError = e; throw e; | |
} finally { | |
loadDepth--; | |
// console.log(lang.string.indent("[lively.vm commonjs] done loading %s", " ", loadDepth), filename); | |
// delete global[env.recorderName]; | |
} | |
} | |
function customLoad(request, parent, isMain) { | |
// console.log("LOADING ", request, parent ? parent.id : "NO PARENT") | |
var id = resolve(request, parent), | |
parentId = resolve(parent.id); | |
if (exceptions.some(exc => exc(id)) || exceptions.some(exc => exc(parentId))) | |
return originalLoad.call(this, request, parent, isMain); | |
if (debug) { | |
var parentRel = relative(process.cwd(), parentId); | |
console.log(lang.string.indent("[lively.vm cjs dependency] %s -> %s", " ", loadDepth), parentRel, request); | |
// console.log(id); | |
} | |
if (!requireMap[parent.id]) requireMap[parent.id] = [id]; | |
else requireMap[parent.id].push(id); | |
return originalLoad.call(this, request, parent, isMain); | |
} | |
function wrapModuleLoad() { | |
if (!originalCompile) | |
originalCompile = Module.prototype._compile; | |
Module.prototype._compile = customCompile; | |
if (!originalLoad) | |
originalLoad = Module._load; | |
Module._load = customLoad; | |
} | |
function unwrapModuleLoad() { | |
if (originalCompile) | |
Module.prototype._compile = originalCompile; | |
if (originalLoad) | |
Module._load = originalLoad; | |
} | |
function envFor(moduleName) { | |
// var fullName = require.resolve(moduleName); | |
var fullName = resolve(moduleName); | |
return ensureEnv(fullName); | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// instrumented code evaluation | |
function runEval(code, options) { | |
options = lang.obj.merge({targetModule: null, parentModule: null}, options); | |
return Promise.resolve().then(() => { | |
if (!options.targetModule) { | |
options.targetModule = scratchModule; | |
} else { | |
options.targetModule = resolve(options.targetModule, options.parentModule); | |
} | |
var fullName = resolve(options.targetModule); | |
if (!nodeModules.cache[fullName]) { | |
try { | |
nodeModules.require(fullName); | |
} catch (e) { | |
throw new Error(`Cannot load module ${options.targetModule} (tried as ${fullName})\noriginal load error: ${e.stack}`) | |
} | |
} | |
var m = nodeModules.cache[fullName], | |
env = envFor(fullName), | |
rec = env.recorder, | |
recName = env.recorderName; | |
rec.__filename = m.filename; | |
var d = rec.__dirname = dirname(m.filename); | |
// rec.require = function(fname) { | |
// if (!path.isAbsolute(fname)) | |
// fname = path.join(d, fname); | |
// return Module._load(fname, m); | |
// }; | |
rec.exports = m.exports; | |
rec.module = m; | |
GLOBAL[recName] = rec; | |
options = lang.obj.merge(options, { | |
recordGlobals: true, | |
dontTransform: [recName, "global"], | |
varRecorderName: recName, | |
topLevelVarRecorder: rec, | |
sourceURL: options.targetModule, | |
context: rec.exports || {} | |
}); | |
return evaluator.runEval(code, options); | |
}); | |
} | |
function importCjsModule(name) { | |
return new Promise(ok => ok(nodeModules.require(resolve(name)))); | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// module runtime status | |
function status(thenDo) { | |
var files = Object.keys(loadedModules), | |
envs = files.reduce((envs, fn) => { | |
envs[fn] = { | |
loadError: loadedModules[fn].loadError, | |
isLoaded: isLoaded(fn), | |
recorderName: loadedModules[fn].recorderName, | |
isInstrumented: loadedModules[fn].isInstrumented, | |
recordedVariables: Object.keys(loadedModules[fn].recorder) | |
} | |
return envs; | |
}, {}); | |
if (typeof thenDo === "function") thenDo(null, envs); | |
return Promise.resolve(envs); | |
} | |
function statusForPrinted(moduleName, options, thenDo) { | |
options = lang.obj.merge({depth: 3}, options); | |
var env = envFor(moduleName); | |
var state = { | |
loadError: env.loadError, | |
recorderName: env.recorderName, | |
recordedVariables: env.recorder | |
} | |
thenDo(null, lang.obj.inspect(state, {maxDepth: options.depth})); | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// module reloading | |
function _invalidateCacheForModules(fullModuleIds, moduleMap) { | |
fullModuleIds.forEach(id => { | |
delete moduleMap[id]; | |
delete loadedModules[id]; | |
}); | |
} | |
function forgetModule(moduleName, parent) { | |
var id = resolve(moduleName, parent), | |
deps = findDependentsOf(id); | |
_invalidateCacheForModules([id].concat(deps), Module._cache); | |
return id; | |
} | |
function forgetModuleDeps(moduleName, parent) { | |
var id = resolve(moduleName, parent), | |
deps = findDependentsOf(id); | |
_invalidateCacheForModules(deps, Module._cache); | |
return id; | |
} | |
function reloadModule(moduleName, parent) { | |
var id = forgetModule(moduleName, parent); | |
return nodeModules.require(id); | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// module dependencies | |
function findDependentsOf(id) { | |
// which modules (module ids) are (in)directly required by module with id | |
// Let's say you have | |
// module1: exports.x = 23; | |
// module2: exports.y = require("./module1").x + 1; | |
// module3: exports.z = require("./module2").y + 1; | |
// `findDependentsOf` gives you an answer what modules are "stale" when you | |
// change module1 | |
return lang.graph.hull(lang.graph.invert(requireMap), resolve(id)); | |
} | |
function findRequirementsOf(id) { | |
// which modules (module ids) are (in)directly required by module with id | |
// Let's say you have | |
// module1: exports.x = 23; | |
// module2: exports.y = require("./module1").x + 1; | |
// module3: exports.z = require("./module2").y + 1; | |
// `findRequirementsOf("./module3")` will report ./module2 and ./module1 | |
return lang.graph.hull(requireMap, resolve(id)); | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
export { | |
// internals | |
requireMap as _requireMap, | |
loadedModules as _loadedModules, | |
instrumentedFiles, | |
prepareCodeForCustomCompile as _prepareCodeForCustomCompile, | |
getExceptions as _getExceptions, | |
setExceptions as _setExceptions, | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// accessing | |
sourceOf, | |
envFor, | |
status, | |
statusForPrinted, | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// eval + changes | |
runEval, | |
// sourceChange, | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// loading + dependencies | |
// reloadModule, | |
resolve, | |
importCjsModule as import, | |
reloadModule, | |
forgetModule, | |
forgetModuleDeps, | |
findRequirementsOf, | |
findDependentsOf, | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// instrumentation | |
wrapModuleLoad, | |
unwrapModuleLoad | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*global process, before, beforeEach, afterEach, describe, it, expect*/ | |
import { expect } from "mocha-es6"; | |
import * as vm from "lively.vm"; | |
import lang from "lively.lang"; | |
var Global = typeof global !== "undefined" ? global : window; | |
var es6 = vm.es6; | |
var cjs = vm.cjs; | |
var cjsModule1 = "test-resources/es6-with-cjs/module1.js"; | |
var cjsModule2 = "test-resources/cjs/module2.js"; | |
var es6Module1 = "test-resources/es6/module1.js"; | |
var es6Module2 = "test-resources/es6-with-cjs/module2.js"; | |
var cjsModule1Id = cjs.resolve(cjsModule1, __filename);; | |
describe("es6 + node modules", () => { | |
before(function() { | |
es6._init({ | |
baseURL: env.isCommonJS ? 'tests/' : document.URL.replace(/\/[^\/]*$/, ""), | |
transpiler: 'babel', babelOptions: {}, | |
map: env.isCommonJS ? {} : {babel: '../node_modules/babel-core/browser.js'} | |
}); | |
es6.wrapModuleLoad(); | |
cjs.wrapModuleLoad(); | |
}); | |
beforeEach(() => { | |
cjs.forgetModule(cjsModule1); | |
es6.forgetModule(es6Module1); | |
}); | |
describe("cjs loads es6", () => { | |
it("succeeds", () => | |
lang.promise.delay(120, // wait for es6 module to be loaded | |
cjs.import(cjsModule1Id).then(m => expect(m.x).to.equal(1))) | |
.then(() => expect(cjs.envFor(cjsModule1Id).recorder.loaded).to.equal(true)) | |
.then(() => expect(es6.envFor(es6.resolve(es6Module1)).recorder.x).to.equal(3))); | |
}); | |
describe("es6 loads cjs", () => { | |
it("succeeds", () => | |
es6.import(es6Module2) | |
.then(m => expect(m.y).to.equal(43)) | |
.then(() => expect(es6.envFor(es6.resolve(es6Module2)).recorder.val).to.equal(43)) | |
.then(() => expect(cjs.envFor(cjs.resolve(cjsModule2, __filename)).recorder.someVal).to.equal(43))); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import path from "path"; | |
import { arr, obj, string, graph, properties, classHelper } from "lively.lang"; | |
import * as ast from "lively.ast"; | |
import * as evaluator from "./evaluator"; | |
import * as cjs from "./commonjs-interface.js"; | |
import { Module } from "module"; | |
var __require = (name, parent) => Module._load(name, parent); | |
var GLOBAL = typeof window !== "undefined" ? window : (typeof Global !== "undefined" ? Global : global); | |
function currentSystem() { return GLOBAL.System; } | |
var SystemLoader = currentSystem().constructor; | |
SystemLoader.prototype.__defineGetter__("__lively_vm__", function() { | |
return { | |
envFor: envFor, | |
evaluationDone: function(moduleId) { | |
var env = envFor(moduleId); | |
addGetterSettersForNewVars(moduleId, env); | |
runScheduledExportChanges(moduleId); | |
}, | |
dumpConfig: function() { | |
var System = currentSystem(), | |
json = { | |
baseURL: System.baseURL, | |
transpiler: System.transpiler, | |
map: System.map, | |
meta: System.meta, | |
packages: System.packages, | |
paths: System.paths, | |
packageConfigPaths: System.packageConfigPaths | |
} | |
return JSON.stringify(json, null, 2); | |
}, | |
loadedModules: this.__lively_vm__loadedModules || (this.__lively_vm__loadedModules = {}) | |
} | |
}); | |
var isNode = currentSystem().get("@system-env").node; | |
var debug = false; | |
// var debug = true; | |
function relative(a, b) { | |
return !path || !path.relative ? b : relative(a,b); | |
} | |
function relativeName(name) { | |
var base = currentSystem().baseURL.replace(/^[\w]+:\/\//, ""), | |
abs = name.replace(/^[\w]+:\/\//, ""); | |
return relative(base, abs); | |
} | |
function join(pathA, pathB) { | |
return pathA.replace(/\/$/, "") + "/" + pathB.replace(/^\//, ""); | |
} | |
var node_modulesDir = resolve("lively.vm/node_modules/"); | |
var exceptions = [ | |
// id => id.indexOf(resolve("node_modules/")) > -1, | |
id => canonicalURL(id).indexOf(node_modulesDir) > -1, | |
id => string.include(id, "babel-core/browser.js") || string.include(id, "system.src.js"), | |
// id => lang.string.include(id, "lively.ast.es6.bundle.js"), | |
id => id.slice(-3) !== ".js" | |
], | |
pendingConfigs = [], configInitialized = false, | |
esmFormatCommentRegExp = /['"]format (esm|es6)['"];/, | |
cjsFormatCommentRegExp = /['"]format cjs['"];/, | |
// Stolen from SystemJS | |
esmRegEx = /(^\s*|[}\);\n]\s*)(import\s+(['"]|(\*\s+as\s+)?[^"'\(\)\n;]+\s+from\s+['"]|\{)|export\s+\*\s+from\s+["']|export\s+(\{|default|function|class|var|const|let|async\s+function))/; | |
function getExceptions() { return exceptions; } | |
function setExceptions(v) { return exceptions = v; } | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// configuration | |
function init(cfg) { | |
var SystemLoader = currentSystem().constructor; | |
debug && console.log("[lively.vm es6] defining new System"); | |
GLOBAL.System = new SystemLoader(); | |
currentSystem().trace = true; | |
// _currentSystem.__defineGetter__("__lively_vm__", () => module.exports); | |
cfg = obj.merge({transpiler: 'babel', babelOptions: {}}, cfg); | |
if (currentSystem().get("@system-env").node) { | |
var nodejsCoreModules = ["addons", "assert", "buffer", "child_process", | |
"cluster", "console", "crypto", "dgram", "dns", "domain", "events", "fs", | |
"http", "https", "module", "net", "os", "path", "punycode", "querystring", | |
"readline", "repl", "stream", "stringdecoder", "timers", "tls", | |
"tty", "url", "util", "v8", "vm", "zlib"], | |
map = nodejsCoreModules.reduce((map, ea) => { map[ea] = "@node/" + ea; return map; }, {}); | |
cfg.map = obj.merge(map, cfg.map); | |
// for sth l ike map: {"lively.lang": "node_modules:lively.lang"} | |
cfg.paths = obj.merge({"node_modules:*": "./node_modules/*"}, cfg.paths); | |
cfg.packageConfigPaths = cfg.packageConfigPaths || ['./node_modules/*/package.json']; | |
if (!cfg.hasOwnProperty("defaultJSExtensions")) cfg.defaultJSExtensions = true; | |
} | |
config(cfg); | |
} | |
function config(cfg) { | |
// First config call needs to have baseURL. To still allow setting other | |
// config parameters we cache non-baseURL calls that come before and run them | |
// as soon as we get the baseURL | |
if (!configInitialized && !cfg.baseURL) { | |
debug && console.log("[lively.vm es6 config call queued]"); | |
pendingConfigs.push(cfg); | |
return; | |
} | |
debug && console.log("[lively.vm es6 System] config"); | |
currentSystem().config(cfg); | |
if (!configInitialized) { | |
configInitialized = true; | |
pendingConfigs.forEach(ea => currentSystem().config(ea)); | |
} | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// loading | |
function importES6Module(path, options) { | |
if (typeof options !== "undefined") config(options); | |
return currentSystem().import(path); | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// module environment + runtime state | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
function instrumentedFiles() { return Object.keys(currentSystem().__lively_vm__.loadedModules); } | |
function isLoaded(fullname) { return fullname in currentSystem().__lively_vm__.loadedModules; } | |
function canonicalURL(url) { | |
// removes double slashes, doesn't resolve relative parts yet | |
var m = url.match(/([^:]+:\/\/)(.*)/); | |
if (m) { | |
var protocol = m[1]; | |
url = m[2]; | |
} | |
url = url.replace(/([^:])\/[\/]+/g, "$1/"); | |
return (protocol || "") + url; | |
} | |
function resolve(name, parentName, parentAddress) { | |
// if (name.match(/^([\w+_]+:)?\/\//)) return name; | |
return canonicalURL(currentSystem().normalizeSync(name, parentName, parentAddress)); | |
} | |
function addGetterSettersForNewVars(moduleId, env) { | |
// after eval we modify the env so that all captures vars are wrapped in | |
// getter/setter to be notified of changes | |
// FIXME: better to not capture via assignments but use func calls...! | |
var prefix = "__lively.vm__"; | |
Object.keys(env).forEach(key => { | |
if (key.indexOf(prefix) === 0 || env.__lookupGetter__(key)) return; | |
env[prefix + key] = env[key]; | |
env.__defineGetter__(key, () => env[prefix + key]); | |
env.__defineSetter__(key, (v) => { | |
scheduleModuleExportsChange(moduleId, key, v, false/*add export*/); | |
return env[prefix + key] = v; | |
}); | |
}); | |
} | |
function envFor(fullname) { | |
if (currentSystem().__lively_vm__.loadedModules[fullname]) return currentSystem().__lively_vm__.loadedModules[fullname]; | |
var env = currentSystem().__lively_vm__.loadedModules[fullname] = { | |
loadError: undefined, | |
recorderName: "__rec__", | |
// recorderName: "__lvVarRecorder", | |
dontTransform: ["__lively_vm__", "__rec__", "__lvVarRecorder", "global", "System", "_moduleExport", "_moduleImport"].concat(ast.query.knownGlobals), | |
recorder: Object.create(GLOBAL, { | |
_moduleExport: { | |
get() { return (name, val) => scheduleModuleExportsChange(fullname, name, val, true/*add export*/); } | |
}, | |
_moduleImport: { | |
get: function() { | |
return (moduleName, name) => { | |
var fullModuleName = resolve(moduleName, fullname), | |
imported = currentSystem()._loader.modules[fullModuleName]; | |
if (!imported) throw new Error(`import of ${name} failed: ${moduleName} (tried as ${fullModuleName}) is not loaded!`); | |
if (name == undefined) | |
return imported.module; | |
if (!imported.module.hasOwnProperty(name)) | |
console.warn(`import from ${moduleName}: Has no export ${name}!`); | |
return imported.module[name]; | |
// var fullModuleName = resolve(moduleName, fullname), | |
// rec = moduleRecordFor(fullModuleName); | |
// if (!rec) throw new Error(`import of ${name} failed: ${moduleName} (tried as ${fullModuleName}) is not loaded!`); | |
// return rec.exports[name]; | |
} | |
} | |
} | |
}) | |
} | |
return env; | |
} | |
function moduleRecordFor(fullname) { | |
var record = currentSystem()._loader.moduleRecords[fullname]; | |
if (!record) return null; | |
if (!record.hasOwnProperty("__lively_vm__")) record.__lively_vm__ = { | |
evalOnlyExport: {} | |
}; | |
return record; | |
} | |
function updateModuleRecordOf(fullname, doFunc) { | |
var record = moduleRecordFor(fullname); | |
if (!record) throw new Error(`es6 environment global of ${fullname}: module not loaded, cannot get export object!`); | |
record.locked = true; | |
try { | |
doFunc(record); | |
} finally { record.locked = false; } | |
} | |
function sourceOf(moduleName, parent) { | |
var name = resolve(moduleName), | |
load = (currentSystem().loads && currentSystem().loads[name]) || { | |
status: 'loading', address: name, name: name, | |
linkSets: [], dependencies: [], metadata: {}}; | |
return currentSystem().fetch(load); | |
} | |
function importsAndExportsOf(moduleName) { | |
return currentSystem().normalize(moduleName) | |
.then(id => | |
Promise.resolve(sourceOf(id)) | |
.then(source => { | |
var parsed = ast.parse(source), | |
scope = ast.query.scopes(parsed); | |
// compute imports | |
var imports = scope.importDecls.reduce((imports, node) => { | |
var nodes = ast.query.nodesAtIndex(parsed, node.start); | |
var importStmt = arr.without(nodes, scope.node)[0]; | |
if (!importStmt) return imports; | |
var from = importStmt.source ? importStmt.source.value : "unknown module"; | |
if (!importStmt.specifiers.length) // no imported vars | |
return imports.concat([{ | |
localModule: id, | |
local: null, | |
imported: null, | |
fromModule: from, | |
importStatement: importStmt | |
}]); | |
return imports.concat(importStmt.specifiers.map(importSpec => { | |
var imported; | |
if (importSpec.type === "ImportNamespaceSpecifier") imported = "*"; | |
else if (importSpec.type === "ImportDefaultSpecifier") imported = "default"; | |
else if (importStmt.source) imported = importStmt.source.name; | |
else imported = null; | |
return { | |
localModule: id, | |
local: importSpec.local ? importSpec.local.name : null, | |
imported: imported, | |
fromModule: from, | |
importStatement: importStmt | |
} | |
})) | |
}, []); | |
var exports = scope.exportDecls.reduce((exports, node) => { | |
var nodes = ast.query.nodesAtIndex(parsed, node.start); | |
var exportsStmt = arr.without(nodes, scope.node)[0]; | |
if (!exportsStmt) return exports; | |
if (exportsStmt.type === "ExportAllDeclaration") { | |
var from = exportsStmt.source ? exportsStmt.source.value : null; | |
return exports.concat([{ | |
localModule: id, | |
local: null, | |
exported: "*", | |
fromModule: from, | |
exportStatement: exportsStmt | |
}]) | |
} | |
return exports.concat(exportsStmt.specifiers.map(exportSpec => { | |
return { | |
localModule: id, | |
local: exportSpec.local ? exportSpec.local.name : null, | |
exported: exportSpec.exported ? exportSpec.exported.name : null, | |
fromModule: id, | |
exportStatement: exportsStmt | |
} | |
})) | |
}, []); | |
return { | |
imports: arr.uniqBy(imports, (a, b) => a.local == b.local && a.imported == b.imported && a.fromModule == b.fromModule), | |
exports: arr.uniqBy(exports, (a, b) => a.local == b.local && a.exported == b.exported && a.fromModule == b.fromModule) | |
} | |
})) | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// update exports | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
var pendingExportChanges = {}; | |
function scheduleModuleExportsChange(moduleId, name, value, addNewExport) { | |
var rec = moduleRecordFor(moduleId); | |
if (rec && (name in rec.exports || addNewExport)) { | |
var pending = pendingExportChanges[moduleId] || (pendingExportChanges[moduleId] = {}); | |
pending[name] = value; | |
} | |
} | |
function runScheduledExportChanges(moduleId) { | |
var keysAndValues = pendingExportChanges[moduleId]; | |
if (!keysAndValues) return; | |
clearPendingModuleExportChanges(moduleId); | |
updateModuleExports(moduleId, keysAndValues); | |
} | |
function clearPendingModuleExportChanges(moduleId) { | |
delete pendingExportChanges[moduleId]; | |
} | |
function updateModuleExports(moduleId, keysAndValues) { | |
updateModuleRecordOf(moduleId, (record) => { | |
var newExports = [], existingExports = []; | |
Object.keys(keysAndValues).forEach(name => { | |
var value = keysAndValues[name]; | |
debug && console.log("[lively.vm es6 updateModuleExports] %s export %s = %s", relativeName(moduleId), name, String(value).slice(0,30).replace(/\n/g, "") + "..."); | |
var isNewExport = !(name in record.exports); | |
if (isNewExport) record.__lively_vm__.evalOnlyExport[name] = true; | |
// var isEvalOnlyExport = record.__lively_vm__.evalOnlyExport[name]; | |
record.exports[name] = value; | |
if (isNewExport) newExports.push(name); | |
else existingExports.push(name); | |
}); | |
// if it's a new export we don't need to update dependencies, just the | |
// module itself since no depends know about the export... | |
// HMM... what about *-imports? | |
newExports.forEach(name => { | |
var oldM = currentSystem()._loader.modules[moduleId].module, | |
m = currentSystem()._loader.modules[moduleId].module = new oldM.constructor(), | |
pNames = Object.getOwnPropertyNames(record.exports); | |
for (var i = 0; i < pNames.length; i++) (function(key) { | |
Object.defineProperty(m, key, { | |
configurable: false, enumerable: true, | |
get() { return record.exports[key]; } | |
}); | |
})(pNames[i]); | |
// Object.defineProperty(System._loader.modules[fullname].module, name, { | |
// configurable: false, enumerable: true, | |
// get() { return record.exports[name]; } | |
// }); | |
}); | |
// For exising exports we find the execution func of each dependent module and run that | |
// FIXME this means we run the entire modules again, side effects and all!!! | |
if (existingExports.length) { | |
debug && console.log("[lively.vm es6 updateModuleExports] updating %s dependents of %s", record.importers.length, relativeName(moduleId)); | |
for (var i = 0, l = record.importers.length; i < l; i++) { | |
var importerModule = record.importers[i]; | |
if (!importerModule.locked) { | |
var importerIndex = importerModule.dependencies.indexOf(record); | |
importerModule.setters[importerIndex](record.exports); | |
importerModule.execute(); | |
} | |
} | |
} | |
}); | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// code instrumentation | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
function prepareCodeForCustomCompile(source, fullname, env) { | |
source = String(source); | |
var tfmOptions = { | |
topLevelVarRecorder: env.recorder, | |
varRecorderName: env.recorderName, | |
dontTransform: env.dontTransform, | |
recordGlobals: true | |
}, | |
header = (debug ? `console.log("[lively.vm es6] executing module ${relativeName(fullname)}");\n` : "") | |
+ `var __lively_vm__ = System.__lively_vm__, ${env.recorderName} = __lively_vm__.envFor("${fullname}").recorder;\n`, | |
footer = `\n__lively_vm__.evaluationDone("${fullname}");`; | |
try { | |
return header + evaluator.evalCodeTransform(source, tfmOptions) + footer; | |
} catch (e) { | |
console.error("Error in prepareCodeForCustomCompile", e.stack); | |
return source; | |
} | |
} | |
function getCachedNodejsModule(load) { | |
// On nodejs we might run alongside normal node modules. To not load those | |
// twice we have this little hack... | |
try { | |
var Module = __require("module").Module, | |
id = Module._resolveFilename(load.name.replace(/^file:\/\//, "")), | |
nodeModule = Module._cache[id]; | |
return nodeModule; | |
} catch (e) { | |
debug && console.log("[lively.vm es6 getCachedNodejsModule] %s unknown to nodejs", relativeName(load.name)); | |
} | |
return null; | |
} | |
function addNodejsWrapperSource(load) { | |
// On nodejs we might run alongside normal node modules. To not load those | |
// twice we have this little hack... | |
var m = getCachedNodejsModule(load); | |
if (m) { | |
load.source = `export default System._nodeRequire('${m.id}');\n`; | |
load.source += properties.allOwnPropertiesOrFunctions(m.exports).map(k => | |
classHelper.isValidIdentifier(k) ? | |
`export var ${k} = System._nodeRequire('${m.id}')['${k}'];` : | |
`/*ignoring export "${k}" b/c it is not a valid identifier*/`).join("\n") | |
debug && console.log("[lively.vm es6 customTranslate] loading %s from nodejs module cache", relativeName(load.name)); | |
return true; | |
} | |
debug && console.log("[lively.vm es6 customTranslate] %s not yet in nodejs module cache", relativeName(load.name)); | |
return false; | |
} | |
function customTranslate(proceed, load) { | |
// load like | |
// { | |
// address: "file:///Users/robert/Lively/lively-dev/lively.vm/tests/test-resources/some-es6-module.js", | |
// name: "file:///Users/robert/Lively/lively-dev/lively.vm/tests/test-resources/some-es6-module.js", | |
// metadata: { deps: [/*...*/], entry: {/*...*/}, format: "esm", sourceMap: ... }, | |
// source: "..." | |
// } | |
if (exceptions.some(exc => exc(load.name))) { | |
debug && console.log("[lively.vm es6 customTranslate ignoring] %s", relativeName(load.name)); | |
return proceed(load); | |
} | |
if (currentSystem().get("@system-env").node && addNodejsWrapperSource(load)) { | |
debug && console.log("[lively.vm es6] loaded %s from nodejs cache", relativeName(load.name)) | |
return proceed(load); | |
} | |
var start = Date.now(); | |
var isEsm = load.metadata.format == 'esm' || load.metadata.format == 'es6' | |
|| (!load.metadata.format && esmFormatCommentRegExp.test(load.source.slice(0,5000))) | |
|| (!load.metadata.format && !cjsFormatCommentRegExp.test(load.source.slice(0,5000)) && esmRegEx.test(load.source)), | |
isCjs = load.metadata.format == 'cjs', | |
isGlobal = load.metadata.format == 'global'; | |
// console.log(load.name + " isEsm? " + isEsm) | |
if (isEsm) { | |
load.metadata.format = "esm"; | |
load.source = prepareCodeForCustomCompile(load.source, load.name, envFor(load.name)); | |
load.metadata["lively.vm instrumented"] = true; | |
debug && console.log("[lively.vm es6] loaded %s as es6 module", relativeName(load.name)) | |
// debug && console.log(load.source) | |
} else if (isCjs && isNode) { | |
load.metadata.format = "cjs"; | |
var id = cjs.resolve(load.address.replace(/^file:\/\//, "")); | |
load.source = cjs._prepareCodeForCustomCompile(load.source, id, cjs.envFor(id)); | |
load.metadata["lively.vm instrumented"] = true; | |
debug && console.log("[lively.vm es6] loaded %s as instrumented cjs module", relativeName(load.name)) | |
// console.log("[lively.vm es6] no rewrite for cjs module", load.name) | |
} else if (isGlobal) { | |
load.source = prepareCodeForCustomCompile(load.source, load.name, envFor(load.name)); | |
load.metadata["lively.vm instrumented"] = true; | |
} else { | |
debug && console.log("[lively.vm es6] customTranslate ignoring %s b/c don't know how to handle global format", relativeName(load.name)); | |
} | |
debug && console.log("[lively.vm es6 customTranslate] done %s after %sms", relativeName(load.name), Date.now()-start); | |
return proceed(load); | |
} | |
function wrapModuleLoad() { | |
if (!currentSystem().origTranslate) { | |
currentSystem().origTranslate = currentSystem().translate | |
currentSystem().translate = function(load) { | |
return customTranslate(currentSystem().origTranslate.bind(currentSystem()), load); | |
} | |
} | |
} | |
function unwrapModuleLoad() { | |
if (currentSystem().origTranslate) { | |
currentSystem().translate = currentSystem().origTranslate; | |
delete currentSystem().origTranslate; | |
} | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// evaluation | |
function ensureImportsAreLoaded(code, parentModule) { | |
var body = ast.parse(code).body, | |
imports = body.filter(node => node.type === "ImportDeclaration"); | |
return Promise.all(imports.map(node => { | |
var fullName = resolve(node.source.value, parentModule); | |
return moduleRecordFor(fullName) ? undefined : currentSystem().import(fullName); | |
})).catch(err => { | |
console.error("Error ensuring imports: " + err.message); | |
throw err; | |
}); | |
} | |
function runEval(code, options) { | |
options = obj.merge({ | |
targetModule: null, parentModule: null, | |
parentAddress: null | |
}, options); | |
return Promise.resolve().then(() => { | |
// if (!options.targetModule) return reject(new Error("options.targetModule not defined")); | |
if (!options.targetModule) { | |
options.targetModule = "*scratch*" | |
// resolve(options.targetModule); | |
} else { | |
options.targetModule = resolve(options.targetModule, options.parentModule || currentSystem().baseURL, options.parentAddress); | |
} | |
var fullname = options.targetModule; | |
// throw new Error(`Cannot load module ${options.targetModule} (tried as ${fullName})\noriginal load error: ${e.stack}`) | |
return importES6Module(fullname) | |
.then(() => ensureImportsAreLoaded(code, options.targetModule)) | |
.then(() => { | |
var env = envFor(fullname), | |
rec = env.recorder, | |
recName = env.recorderName, | |
header = `var ${recName} = System.__lively_vm__.envFor("${fullname}").recorder,\n` | |
+ ` _moduleExport = ${recName}._moduleExport,\n` | |
+ ` _moduleImport = ${recName}._moduleImport;\n`; | |
options = obj.merge( | |
{waitForPromise: true}, | |
options, { | |
recordGlobals: true, | |
dontTransform: env.dontTransform, | |
varRecorderName: recName, | |
topLevelVarRecorder: rec, | |
sourceURL: options.sourceURL || options.targetModule, | |
context: rec, | |
es6ExportFuncId: "_moduleExport", | |
es6ImportFuncId: "_moduleImport", | |
header: header | |
}); | |
clearPendingModuleExportChanges(fullname); | |
return evaluator.runEval(code, options).then(result => { | |
currentSystem().__lively_vm__.evaluationDone(fullname); return result; }) | |
}) | |
// .catch(err => console.error(err) || err) | |
}); | |
} | |
function sourceChange(moduleName, newSource, options) { | |
var fullname = resolve(moduleName), | |
load = { | |
status: 'loading', | |
source: newSource, | |
name: fullname, | |
linkSets: [], | |
dependencies: [], | |
metadata: {format: "esm"} | |
}; | |
return (currentSystem().get(fullname) ? Promise.resolve() : importES6Module(fullname)) | |
.then((_) => _systemTranslateParsed(load)) | |
.then(updateData => { | |
var record = moduleRecordFor(fullname), | |
_exports = (name, val) => scheduleModuleExportsChange(fullname, name, val), | |
declared = updateData.declare(_exports); | |
currentSystem().__lively_vm__.evaluationDone(fullname); | |
// ensure dependencies are loaded | |
debug && console.log("[lively.vm es6] sourceChange of %s with deps", fullname, updateData.localDeps); | |
return Promise.all( | |
// gather the data we need for the update, this includes looking up the | |
// imported modules and getting the module record and module object as | |
// a fallback (module records only exist for esm modules) | |
updateData.localDeps.map(depName => | |
currentSystem().normalize(depName, fullname) | |
.then(depFullname => { | |
var depModule = currentSystem().get(depFullname), | |
record = moduleRecordFor(depFullname); | |
return depModule && record ? | |
{name: depName, fullname: depFullname, module: depModule, record: record} : | |
importES6Module(depFullname).then((module) => ({ | |
name: depName, | |
fullname: depFullname, | |
module: currentSystem().get(depFullname) || module, | |
record: moduleRecordFor(depFullname) | |
})); | |
}))) | |
.then(deps => { | |
// 1. update dependencies | |
record.dependencies = deps.map(ea => ea.record); | |
// hmm... for house keeping... not really needed right now, though | |
var load = currentSystem().loads && currentSystem().loads[fullname]; | |
if (load) { | |
load.deps = deps.map(ea => ea.name); | |
load.depMap = deps.reduce((map, dep) => { map[dep.name] = dep.fullname; return map; }, {}); | |
if (load.metadata && load.metadata.entry) { | |
load.metadata.entry.deps = load.deps; | |
load.metadata.entry.normalizedDeps = deps.map(ea => ea.fullname); | |
load.metadata.entry.declare = updateData.declare; | |
} | |
} | |
// 2. run setters to populate imports | |
deps.forEach((d,i) => declared.setters[i](d.module)); | |
// 3. execute module body | |
return declared.execute(); | |
}); | |
}); | |
} | |
function _systemTranslateParsed(load) { | |
// brittle! | |
// The result of System.translate is source code for a call to | |
// System.register that can't be run standalone. We parse the necessary | |
// details from it that we will use to re-define the module | |
// (dependencies, setters, execute) | |
return currentSystem().translate(load).then(translated => { | |
// translated looks like | |
// (function(__moduleName){System.register(["./some-es6-module.js", ...], function (_export) { | |
// "use strict"; | |
// var x, z, y; | |
// return { | |
// setters: [function (_someEs6ModuleJs) { ... }], | |
// execute: function () {...} | |
// }; | |
// }); | |
var parsed = ast.parse(translated), | |
call = parsed.body[0].expression, | |
moduleName = call.arguments[0].value, | |
registerCall = call.callee.body.body[0].expression, | |
depNames = arr.pluck(registerCall["arguments"][0].elements, "value"), | |
declareFuncNode = call.callee.body.body[0].expression["arguments"][1], | |
declareFuncSource = translated.slice(declareFuncNode.start, declareFuncNode.end), | |
declare = eval(`var __moduleName = "${moduleName}";(${declareFuncSource});\n//@ sourceURL=${moduleName}\n`); | |
if (typeof $morph !== "undefined" && $morph("log")) $morph("log").textString = declare; | |
return {localDeps: depNames, declare: declare}; | |
}); | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// module dependencies | |
function modulesMatching(stringOrRegExp) { | |
var re = stringOrRegExp instanceof RegExp ? stringOrRegExp : new RegExp(stringOrRegExp); | |
return Object.keys(currentSystem()._loader.modules).filter(ea => stringOrRegExp.test(ea)); | |
} | |
function forgetEnvOf(fullname) { | |
delete currentSystem().__lively_vm__.loadedModules[fullname] | |
} | |
function forgetModuleDeps(moduleName, opts) { | |
opts = obj.merge({forgetDeps: true, forgetEnv: true}, opts) | |
var id = resolve(moduleName), | |
deps = findDependentsOf(id); | |
deps.forEach(ea => { | |
currentSystem().delete(ea); | |
if (currentSystem().loads) delete currentSystem().loads[ea]; | |
opts.forgetEnv && forgetEnvOf(ea); | |
}); | |
return id; | |
} | |
function forgetModule(moduleName, opts) { | |
opts = obj.merge({forgetDeps: true, forgetEnv: true}, opts); | |
var id = opts.forgetDeps ? forgetModuleDeps(moduleName, opts) : resolve(moduleName); | |
currentSystem().delete(moduleName); | |
currentSystem().delete(id); | |
if (currentSystem().loads) { | |
delete currentSystem().loads[moduleName]; | |
delete currentSystem().loads[id]; | |
} | |
if (opts.forgetEnv) { | |
forgetEnvOf(id); | |
forgetEnvOf(moduleName); | |
} | |
} | |
function reloadModule(moduleName, opts) { | |
opts = obj.merge({reloadDeps: true, resetEnv: true}, opts); | |
var id = resolve(moduleName), | |
toBeReloaded = [id]; | |
if (opts.reloadDeps) toBeReloaded = findDependentsOf(id).concat(toBeReloaded); | |
forgetModule(id, {forgetDeps: opts.reloadDeps, forgetEnv: opts.resetEnv}); | |
return Promise.all(toBeReloaded.map(ea => ea !== id && importES6Module(ea))) | |
.then(() => importES6Module(id)); | |
} | |
// function computeRequireMap() { | |
// return Object.keys(_currentSystem.loads).reduce((requireMap, k) => { | |
// requireMap[k] = lang.obj.values(_currentSystem.loads[k].depMap); | |
// return requireMap; | |
// }, {}); | |
// } | |
function computeRequireMap() { | |
if (currentSystem().loads) { | |
var store = currentSystem().loads, | |
modNames = arr.uniq(Object.keys(currentSystem().__lively_vm__.loadedModules).concat(Object.keys(store))); | |
return modNames.reduce((requireMap, k) => { | |
var depMap = store[k] ? store[k].depMap : {}; | |
requireMap[k] = Object.keys(depMap).map(localName => { | |
var resolvedName = depMap[localName]; | |
if (resolvedName === "@empty") return `${resolvedName}/${localName}`; | |
return resolvedName; | |
}) | |
return requireMap; | |
}, {}); | |
} | |
return Object.keys(currentSystem()._loader.moduleRecords).reduce((requireMap, k) => { | |
requireMap[k] = currentSystem()._loader.moduleRecords[k].dependencies.filter(Boolean).map(ea => ea.name); | |
return requireMap; | |
}, {}); | |
} | |
function findDependentsOf(id) { | |
// which modules (module ids) are (in)directly import module with id | |
// Let's say you have | |
// module1: export var x = 23; | |
// module2: import {x} from "module1.js"; export var y = x + 1; | |
// module3: import {y} from "module2.js"; export var z = y + 1; | |
// `findDependentsOf` gives you an answer what modules are "stale" when you | |
// change module1 = module2 + module3 | |
return graph.hull(graph.invert(computeRequireMap()), resolve(id)); | |
} | |
function findRequirementsOf(id) { | |
// which modules (module ids) are (in)directly required by module with id | |
// Let's say you have | |
// module1: export var x = 23; | |
// module2: import {x} from "module1.js"; export var y = x + 1; | |
// module3: import {y} from "module2.js"; export var z = y + 1; | |
// `findRequirementsOf("./module3")` will report ./module2 and ./module1 | |
return graph.hull(computeRequireMap(), resolve(id)); | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// packaging | |
function importPackage(packageLocation, options) { | |
options = obj.deepMerge({modules: [], config: {}}, options); | |
// add the package to the System's config | |
currentSystem().config({ | |
packages: { | |
[packageLocation]: options.config | |
}, | |
// hmm, indicate package.json, what if non existing? | |
packageConfigPaths: [join(packageLocation, "package.json")] | |
}); | |
// either load default entrypoint or when options.modules is defined chain- | |
// load those modules | |
var mods = options.modules.map(ea => | |
ea.indexOf(packageLocation) === 0 ? | |
ea : join(packageLocation, ea)); | |
return mods.length ? | |
new Promise((resolve, reject) => { | |
var loadedModules = []; | |
mods.reduce( // load chain | |
(nextLoad, modName) => | |
() => importES6Module(modName) | |
.then(mod => loadedModules.push(mod)) | |
.then(nextLoad), | |
() => resolve(loadedModules))().catch(reject) | |
}) : importES6Module(packageLocation); | |
} | |
/* | |
importPackage("http://localhost:9001/acorn", {modules: ["dist/acorn.js", "dist/walk.js", "dist/acorn_loose.js"]}) | |
.then(show.curry("%o")) | |
.catch(show.curry("%s")) | |
forgetModule("http://localhost:9001/acorn/src/index.js") | |
Object.keys(computeRequireMap()).grep("9001/acorn").forEach(ea => forgetModule(ea)); | |
var c = {map: {"./src.js": "http://localhost:9001/acorn/src/index.js"}} | |
var opts = {modules: ["src/index.js", "src/walk/index.js", "src/loose/index.js"], config: c} | |
importPackage("http://localhost:9001/acorn", opts) | |
.then(show.curry("%s")) | |
.catch(show.curry("%s")) | |
importPackage("http://localhost:9001/lively.ast-es6", { | |
modules: ["index.js"], | |
config: {} | |
}).then(show.curry("%s")).catch(show.curry("%s")) | |
*/ | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
/* | |
function matchNames(name) { return name.startsWith("http://localhost:9001/acorn") && !name.include("dist/"); } | |
function transformNames(name) { return name.replace(/http:\/\/localhost:9001\//, ""); } | |
// URL.root.withFilename("traced.json").asWebResource().put(JSON.stringify(trace, null, 2)); | |
var outfile = 'test-acorn-bundle.js' | |
var c = { | |
baseURL: ".", | |
transpiler: "babel", | |
defaultJSExtensions: true, | |
map: { | |
babel: join(lively.shell.WORKSPACE_LK, "node_modules/babel-core/browser.js") | |
} | |
} | |
exportTrace("acorn", outfile, matchNames, transformNames, c, {transpiler: "babel", defaultJSExtensions: true}).then(() => show("OK")).catch(show.curry("%s")) | |
*/ | |
function exportTrace(bundleName, outfile, matchNames, transformNames, builderConfig, bundleConfig) { | |
var trace = Object.keys(currentSystem().loads) | |
.filter(matchNames) | |
.reduce((trace, name) => { | |
var load = currentSystem().loads[name]; | |
load.metadata && load.metadata && load.metadata.entry && (delete load.metadata.entry); | |
// delete load.source | |
load.source = getOrigSource(name) | |
if (load.name) load.name = transformNames(load.name); | |
if (load.address) load.address = transformNames(load.address); | |
load.depMap = Object.keys(load.depMap).reduce((depMap, name) => { | |
depMap[name] = transformNames(load.depMap[name]); | |
return depMap; | |
}, {}); | |
// load.metadata && load.metadata && load.metadata.entry && (load.metadata.entry.module = null); | |
trace[name] = load; | |
return trace; | |
}, {}); | |
return _runBuilderInNodejs(bundleName, outfile, trace, builderConfig, bundleConfig); | |
function getOrigSource(address) { | |
return new URL(address).asWebResource().get().content | |
} | |
} | |
function _runBuilderInNodejs(bundleName, outfile, trace, builderConfig, bundleConfig) { | |
var names = Object.keys(trace).map(name => trace[name].name); | |
if (!bundleConfig.bundles) bundleConfig.bundles = {}; | |
bundleConfig.bundles[bundleName] = names; | |
var program = `var trace = ${JSON.stringify(trace, null, 2)};\n`; | |
program += `var outputFile = '${outfile}';\n` | |
program += `var builderConfig = ${JSON.stringify(builderConfig, null, 2)};\n`; | |
program += `var bundleConfig = ${JSON.stringify(bundleConfig, null, 2)};\n`; | |
program += `var livelyVMDir = '${join(lively.shell.WORKSPACE_LK, "node_modules/lively.vm/")}';\n`; | |
program += `var path = require("path"); | |
var Builder = require(livelyVMDir + 'node_modules/systemjs-builder'); | |
var builder = new Builder(); | |
builder.config(builderConfig); | |
// delete require.cache[require.resolve("/Users/robert/Lively/LivelyKernel2/traced.json")] | |
// var x = require("/Users/robert/Lively/LivelyKernel2/traced.json") | |
// Object.keys(x).forEach(name => { | |
// x[name].source = String(fs.readFileSync(name.replace("http://localhost:9001/", "/Users/robert/Lively/LivelyKernel2/"))) | |
// }); | |
builder.bundle(trace, outputFile) | |
.then(() => { | |
require("fs").appendFileSync(outputFile, '\\nSystem.config(' + JSON.stringify(bundleConfig, null, 2) + ');\\n'); | |
console.log("%s bundled", outputFile); | |
}) | |
.catch(err => { console.error(err); process.exit(1); }); | |
` | |
var programFile = join(lively.shell.WORKSPACE_LK, ".lively.modules-bundle-program.js"); | |
return writeProgram() | |
.then(() => runProgram()) | |
// .then(() => deleteProgram(), (err) => { deleteProgram(); throw err; }); | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
function runProgram() { | |
return new Promise((resolve, reject) => { | |
lively.shell.run("node " + programFile, {cwd: lively.shell.WORKSPACE_LK}, | |
(err, cmd) => cmd.getCode() > 0 ? reject(new Error(cmd.resultString(true))) : resolve()) | |
}) | |
} | |
function writeProgram() { | |
return new Promise((resolve, reject) => | |
lively.shell.writeFile(programFile, program, | |
(cmd) => cmd.getCode() > 0 ? reject(cmd.resultString(true)) : resolve())) | |
} | |
function deleteProgram() { | |
return new Promise((resolve, reject) => lively.shell.rm(programFile, (err) => err ? reject(err) : resolve())); | |
} | |
} | |
function groupIntoPackages(moduleNames, packageNames) { | |
return arr.groupBy(moduleNames, groupFor); | |
function groupFor(moduleName) { | |
var fullname = resolve(moduleName), | |
matching = packageNames.filter(p => fullname.indexOf(p) === 0); | |
return matching.length ? | |
matching.reduce((specific, ea) => ea.length > specific.length ? ea : specific) : | |
"no group"; | |
} | |
} | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// update after source changes... | |
if (currentSystem().origTranslate) { unwrapModuleLoad(); wrapModuleLoad(); } | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
export { | |
// internals | |
currentSystem, | |
init as _init, | |
config, | |
moduleRecordFor as _moduleRecordFor, | |
updateModuleRecordOf as _updateModuleRecordOf, | |
updateModuleExports as _updateModuleExports, | |
computeRequireMap as _computeRequireMap, | |
getExceptions, | |
setExceptions, | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// accessing | |
sourceOf, | |
envFor, | |
// status, | |
// statusForPrinted, | |
importsAndExportsOf, | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// eval + changes | |
runEval, | |
sourceChange, | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// loading + dependencies | |
resolve, | |
modulesMatching, | |
importES6Module as import, | |
// reloadModule: reloadModule, | |
reloadModule, | |
forgetModule, | |
forgetModuleDeps, | |
findRequirementsOf, | |
findDependentsOf, | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// packaging | |
groupIntoPackages, | |
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | |
// instrumentation | |
wrapModuleLoad, | |
unwrapModuleLoad | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*global process, before, beforeEach, afterEach, describe, it, expect*/ | |
import { expect } from "mocha-es6"; | |
import { es6 } from "lively.vm"; | |
import lang from "lively.lang"; | |
var isNode = System.get("@system-env").node; | |
var base = es6.resolve("lively.vm/tests/"); | |
var module1 = base + "test-resources/es6/module1.js"; | |
var module2 = base + "test-resources/es6/module2.js"; | |
var module3 = base + "test-resources/es6/module3.js"; | |
describe("es6 modules", () => { | |
before(function() { | |
// no need to init when we are in Lively | |
if (typeof $world === "undefined") { | |
es6._init(isNode ? {baseURL: './'} : { | |
transpiler: 'babel', babelOptions: {}, | |
baseURL: document.URL.replace(/\/[^\/]*$/, ""), | |
map: {babel: '../node_modules/babel-core/browser.js'} | |
}); | |
es6.wrapModuleLoad(); | |
} | |
}); | |
afterEach(() => { | |
es6.forgetModule(module1); | |
es6.forgetModule(module2); | |
es6.forgetModule(module3); | |
}); | |
it("can be loaded", () => | |
es6.import(module1).then(m => expect(m.x).equals(3))); | |
it("captures internal module state", () => | |
Promise.all([es6.import(module1), es6.resolve(module1)]) | |
.then((exportsAndName) => | |
expect(es6.envFor(exportsAndName[1])) | |
.deep.property('recorder.internalState').equals(1))); | |
describe("eval", () => { | |
it("inside of module", () => | |
es6.runEval("2 + internalState", {targetModule: module1}) | |
.then(result => expect(result.value).equals(3))); | |
it("of export statement", () => | |
// Load module1 and module2 which depends on module1 | |
lang.promise.chain([ | |
() => Promise.all([es6.import(module1), es6.import(module2)]), | |
(modules, state) => { | |
state.m1 = modules[0]; state.m2 = modules[1]; | |
expect(state.m1.x).to.equal(3); | |
expect(state.m2.y).to.equal(5); | |
}, | |
// Modify module1 | |
() => es6.runEval("export var x = 9;", {asString: true, targetModule: module1}), | |
(result, state) => { | |
expect(result.value).to.not.match(/error/i); | |
expect(state.m1.x).to.equal(9, "module1 not updated"); | |
expect(state.m2.y).to.equal(11, "module2 not updated after its dependency changed"); | |
return Promise.all([ | |
es6.import(module1).then(m => expect(m.x).to.equal(9)), | |
es6.import(module2).then(m => expect(m.y).to.equal(11)), | |
]); | |
}])); | |
it("of export statement with new export", () => | |
lang.promise.chain([ | |
() => Promise.all([es6.import(module1), es6.import(module2)]), | |
(modules, state) => { state.m1 = modules[0]; state.m2 = modules[1]; }, | |
() => es6.runEval("export var foo = 3;", {asString: true, targetModule: module1}), | |
(result, state) => { | |
expect(result.value).to.not.match(/error/i); | |
// Hmmm.... frozen modules require us to re-import... damn! | |
// expect(state.m1.foo).to.equal(3, "foo not defined in module1 after eval"); | |
return es6.import(module1) | |
.then((m1) => expect(m1.foo).to.equal(3, "foo not defined in module1 after eval")) | |
}, | |
() => es6.runEval("export var foo = 5;", {asString: true, targetModule: module1}), | |
(result, state) => { | |
expect(result.value).to.not.match(/error/i); | |
// expect(state.m1.foo).to.equal(5, "foo updated in module1 after re-eval"); | |
return es6.import(module1) | |
.then((m1) => expect(m1.foo).to.equal(5, "foo updated in module1 after re-eval")) | |
}])); | |
it("of import statement", () => | |
// test if import is transformed to lookup + if the imported module gets before eval | |
lang.promise.chain([ | |
() => es6.runEval("import {y} from './module2.js'; y", {targetModule: module1}), | |
(result, state) => { | |
expect(result.value).to.not.match(/error/i); | |
expect(result.value).to.equal(5, "imported value"); | |
}])) | |
it("of var being exported", () => | |
// Load module1 and module2 which depends on module1 | |
lang.promise.chain([ | |
() => Promise.all([es6.import(module1), es6.import(module2)]), | |
(modules, state) => { | |
state.m1 = modules[0]; state.m2 = modules[1]; | |
expect(state.m1.x).to.equal(3); | |
expect(state.m2.y).to.equal(5); | |
}, | |
// Modify module1 | |
() => es6.runEval("var x = 9;", {asString: true, targetModule: module1}), | |
(result, state) => { | |
expect(result.value).to.not.match(/error/i); | |
expect(state.m1.x).to.equal(9, "module1 not updated"); | |
expect(state.m2.y).to.equal(11, "module2 not updated after its dependency changed"); | |
return Promise.all([ | |
es6.import(module1).then(m => expect(m.x).to.equal(9)), | |
es6.import(module2).then(m => expect(m.y).to.equal(11)), | |
]); | |
}])); | |
it("of new var that is exported and then changes", () => | |
es6.import(module1) | |
// define a new var that is exported | |
.then(_ => es6.runEval("var foo = 1; export { foo }", {asString: true, targetModule: module1})) | |
.then(() => expect(es6._moduleRecordFor(module1).exports).to.have.property("foo", 1, "of record")) | |
.then(() => es6.import(module1).then(m1 => expect(m1).to.have.property("foo", 1, "of module"))) | |
// now change that var and see if the export is updated | |
.then(() => es6.runEval("var foo = 2;", {asString: true, targetModule: module1})) | |
.then(() => expect(es6._moduleRecordFor(module1).exports).to.have.property("foo", 2, "of record after change")) | |
.then(() => es6.import(module1).then(m1 => expect(m1).to.have.property("foo", 2, "of module after change")))); | |
}); | |
describe("dependencies", () => { | |
it("computes required modules of some module", () => | |
es6.import(module3).then(() => { | |
expect(es6.findRequirementsOf(module3)).to.deep.equal( | |
[es6.resolve(module2), es6.resolve(module1)]); | |
})); | |
it("computes dependent modules of some module", () => | |
es6.import(module3).then(() => { | |
expect(es6.findDependentsOf(module1)).to.deep.equal( | |
[es6.resolve(module2), es6.resolve(module3)]); | |
})); | |
}); | |
describe("code changes", () => { | |
function changeModule1Source() { | |
// "internalState = 1" => "internalState = 2" | |
return es6.sourceOf(module1).then(s => | |
es6.sourceChange(module1, | |
s.replace(/(internalState = )([0-9]+)/, "$12"), | |
{evaluate: true})); | |
} | |
it("modifies module and its exports", () => | |
es6.import(module1).then(m => { | |
expect(es6.envFor(es6.resolve(module1)).recorder.internalState).to.equal(1, "internal state before change"); | |
expect(m.x).to.equal(3, "export state before change"); | |
return changeModule1Source().then(() => { | |
expect(es6.envFor(es6.resolve(module1)).recorder.internalState).to.equal(2, "internal state after change"); | |
expect(m.x).to.equal(5, "export state after change"); | |
}); | |
})); | |
it("modifies imports", () => | |
es6.import(module2) | |
.then(() => | |
expect(es6._moduleRecordFor(es6.resolve(module2)).dependencies.map(ea => ea.name)) | |
.to.deep.equal([es6.resolve(module1)], "deps before")) | |
.then(m => es6.sourceChange(module2, | |
"import { z as x } from './module3.js'; export var y = x + 2;", | |
{evaluate: true})) | |
.then(() => | |
expect(es6._moduleRecordFor(es6.resolve(module2)).dependencies.map(ea => ea.name)) | |
.to.deep.equal([es6.resolve(module3)], "deps after"))); | |
it("affects dependent modules", () => | |
es6.import(module2).then(m => { | |
expect(m.y).to.equal(5, "before change"); | |
return changeModule1Source().then(() => | |
expect(m.y).to.equal(7, "state after change")); | |
})); | |
it("affects eval state", () => | |
es6.import(module1) | |
.then(m => changeModule1Source()) | |
.then(() => es6.runEval("[internalState, x]", {targetModule: module1})) | |
.then(result => expect(result.value).to.deep.equal([2, 5]))); | |
it("reload module dependencies", () => | |
es6.import(module3) | |
.then(m => expect(m.z).to.equal(15)) | |
// we change module1 and check that the value of module3 that indirectly | |
// depends on module1 has changed as well | |
.then(() => es6.sourceOf(module1)) | |
// es6.sourceOf("tests/" + module1) | |
.then(s => s.replace(/(internalState = )([0-9]+)/, "$12")) | |
.then(s => es6.runEval(s, {asString: true, targetModule: module1})) | |
.then(result => expect(result.value).to.not.match(/error/i)) | |
.then(() => es6.forgetModuleDeps(module1)) | |
.then(() => es6.import(module3)) | |
.then(m => expect(m.z).to.equal(21))); | |
}); | |
describe("unload module", () => { | |
it("forgets module and recordings", () => | |
es6.import(module3) | |
.then(() => es6.forgetModule(module2)) | |
.then(_ => { | |
expect(es6._moduleRecordFor(es6.resolve(module2))).to.equal(null); | |
expect(es6._moduleRecordFor(es6.resolve(module3))).to.equal(null); | |
expect(es6.envFor(es6.resolve(module2)).recorder).to.not.have.property("x"); | |
expect(es6.envFor(es6.resolve(module3)).recorder).to.not.have.property("z"); | |
})); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment