Skip to content

Instantly share code, notes, and snippets.

@rksm
Last active May 1, 2016 02:08
Show Gist options
  • Save rksm/e9ab242c5c4856d7550d14e1a781caf7 to your computer and use it in GitHub Desktop.
Save rksm/e9ab242c5c4856d7550d14e1a781caf7 to your computer and use it in GitHub Desktop.
Exporting bundles in register format from loaded modules in SystemJS, see register format spec https://github.com/ModuleLoader/es6-module-loader/wiki/System.register-Explained
/*global System, beforeEach, afterEach, describe, it*/
import { expect } from "mocha-es6";
import { removeDir, removeFile, createFiles, writeFile, modifyFile, readFile, inspect as i } from "./helpers.js";
import { registerPackage } from "../src/packages.js";
import { getSystem, removeSystem, printSystemConfig } from "../src/system.js";
import { bundle } from "../src/bundling.js";
var testDir = System.normalizeSync("lively.modules/tests/");
describe("bundling", function() {
var System;
var files = {
"test-project": {
"file-1.js": "export var y = 1;",
"file-2.js": "import { y } from './file-1.js'; export var x = 1 + y;",
"file-3.js": "exports.x = 23;",
"file-4.js": "exports.y = require('./file-3.js').x;",
"file-5.js": "import { x } from './file-3.js'; export var y = 1 + x;",
}
}
beforeEach(() => {
System = getSystem("test", {baseURL: testDir});
return createFiles(testDir, files);
});
afterEach(() => {
removeSystem("test");
return removeDir(testDir + "test-project");
});
describe("creation", () => {
it("can bundle es6 modules", () =>
bundle(System, "test-project/test-bundle.js", ["test-project/file-1.js", "test-project/file-2.js"])
.then(() => readFile(testDir + "test-project/test-bundle.js"))
.then(content => expect(content)
.to.match(new RegExp(`System.register\\('${testDir}test-project/file-1.js', \\[\\]`))
.to.match(new RegExp(`System.register\\('${testDir}test-project/file-2.js', \\['./file-1.js'\\]`))));
it("can bundle cjs files", () =>
bundle(System, "test-project/test-bundle-2.js", ["test-project/file-3.js", "test-project/file-4.js"])
.then(() => readFile(testDir + "test-project/test-bundle-2.js"))
.then(content => expect(content).to.match(new RegExp(`System.registerDynamic\\('${testDir}test-project/file-4.js', \\['./file-3.js'\\]`))));
it("can bundle es6 + cjs files", () =>
bundle(System, "test-project/test-bundle-3.js", ["test-project/file-5.js", "test-project/file-3.js"])
.then(() => readFile(testDir + "test-project/test-bundle-3.js"))
.then(content => expect(content).to.match(new RegExp(`System.register\\('${testDir}test-project/file-5.js', \\['./file-3.js'\\]`))));
});
describe("loading", () => {
it("loads bundle as part of package", () =>
bundle(System, "test-project/test-bundle-3.js", ["test-project/file-5.js", "test-project/file-3.js"])
.then(() => modifyFile(testDir + "test-project/file-5.js", content => content.replace(/1 \+ x/, "2 + x")))
.then(() => { removeSystem("test"); System = getSystem("test", {baseURL: testDir}); })
.then(() => {
writeFile(testDir + "test-project/package.json", JSON.stringify({
name: "test-project",
lively: {bundles: {"test-bundle-3.js": ["./file-5.js", "./file-3.js"]}}
}, null, 2))
})
.then(() => registerPackage(System, testDir + "test-project"))
.then(() => System.import("test-project/file-5.js"))
.then(m => expect(m.y).to.equal(24)));
});
});
import { Path, arr } from "lively.lang";
import * as ast from "lively.ast";
import { writeFile, removeFile } from "../tests/helpers.js";
export { bundle }
function bundle(System, bundleFile, files) {
// files = [{moduleName, nameInBundle}]
return Promise.all(files.map(ea => {
var moduleName = typeof ea === "string" ? ea : ea.moduleName;
if (!moduleName) throw new Error("Error bundling module " + ea);
var newName = ea.nameInBundle || moduleName;
return System.normalize(moduleName)
.then(url =>
(System.get(url) ? Promise.resolve() : System.import(url))
.then(() => createRegisterModuleFor(System, url, ea.nameInBundle)));
}))
.then(sources =>
System.normalize(bundleFile)
.then(outFile => writeFile(outFile, sources.join('\n'))));
}
function createRegisterModuleFor(System, url, nameInBundle, formatOverride) {
var load = System.loads && System.loads[url];
if (!load) throw new Error(url + " not loaded / traced!");
var format = formatOverride || load.metadata.format;
if (format === "json") {
return createJSONRegisterDynamicModuleFromLoad(System, url, nameInBundle, load);
} else if (format === "esm" || format === "es6") {
return createRegisterModuleFromLoad(load, nameInBundle);
} else if (format === "global") {
return createGlobalRegisterDynamicModule(System, url, nameInBundle, load.source, load.deps);
} else if (format === "cjs") {
return createCommonJSRegisterDynamicModule(System, url, nameInBundle, load.metadata.entry.executingRequire, load.deps);
} else {
throw new Error("Cannot create register module for " + url + " with format " + format);
}
}
function createRegisterModuleFromLoad(load, nameInBundle) {
return new Promise((resolve, reject) => {
var name = load.name;
if (!load.source) return reject(new Error("No source for " + name));
var parsed = ast.parse(load.source);
if ("CallExpression" !== Path("body.0.expression.type").get(parsed))
return reject(new Error("Load source is not a call Expression (" + name + ")"));
if ("CallExpression" !== Path("body.0.expression.callee.body.body.0.expression.type").get(parsed))
return reject(new Error("Load source body inner is not a System.register call expressions (" + name + ")"));
if ("System" !== Path("body.0.expression.callee.body.body.0.expression.callee.object.name").get(parsed))
return reject(new Error("Not a call to System! (" + name + ")"));
if ("register" !== Path("body.0.expression.callee.body.body.0.expression.callee.property.name").get(parsed))
return reject(new Error("Not a call to System.register! (" + name + ")"));
var moduleName = nameInBundle || Path("body.0.expression.arguments.0.value").get(parsed);
if (!moduleName)
return reject(new Error("Could not extract module name from " + name));
var registerCall = Path("body.0.expression.callee.body.body.0.expression").get(parsed)
registerCall["arguments"].unshift({type: "Literal", value: moduleName})
resolve(ast.stringify(registerCall));
});
}
function createGlobalRegisterDynamicModule(System, url, nameInBundle, source, deps) {
var modSource = `var _retrieveGlobal = System.get("@@global-helpers").prepareGlobal(module.id, null, null);\n`
+ `(function() {\n${source}\n})();\n`
+ "return _retrieveGlobal();"
return createRegisterDynamicModuleWithSource(System, url, nameInBundle, modSource, false, deps);
}
function createCommonJSRegisterDynamicModule(System, url, nameInBundle, executingRequire, deps) {
// FIXME! deps...
var load = {status: 'loading', address: url, name: url, linkSets: [], dependencies: [], metadata: {}};
return System.fetch(load).then(source =>
createRegisterDynamicModuleWithSource(System, url, nameInBundle, source, executingRequire, deps));
}
function createJSONRegisterDynamicModuleFromLoad(System, url, nameInBundle, load) {
return createRegisterDynamicModuleWithSource(System, url, nameInBundle, "return " + load.source, false, []);
}
function createRegisterDynamicModuleWithSource(System, url, nameInBundle, source, executingRequire, deps) {
var depsString = "[" + (deps.length ? "'" + deps.join("', '") + "'" : "") + "]";
return Promise.resolve(
`System.registerDynamic('${nameInBundle || url}', ${depsString}, ${!!executingRequire}, `
+ `function(require, exports, module) {\n${source}\n});\n`);
}
/*global System, beforeEach, afterEach, describe, it*/
import { expect } from "mocha-es6";
import { getSystem, removeSystem, moduleRecordFor, moduleEnv, sourceOf } from "../src/system.js";
import { importSync } from "../src/sync-loader.js";
function defineEntries(System, loadLog) {
// a -> b <-> c
System.register('a.js', ['./b.js'], function (_export) {
'use strict';
loadLog.push("declared a");
var b;
return {
setters: [_b => { loadLog.push("setters in a"); b = _b.default; }],
execute: function () { loadLog.push("body a"); _export('default', "exported from a, " + b); }};
})
System.register('b.js', ['./c.js'], function (_export) {
'use strict';
loadLog.push("declared b");
var c;
return {
setters: [_c => { loadLog.push("setters in b"); c = _c.default; }],
execute: function() { loadLog.push("body b"); _export('default', "exported from b, " + c); }};
})
System.register('c.js', ["./b.js"], function (_export) {
'use strict';
loadLog.push("declared c");
var b;
return {
setters: [_b => { loadLog.push("setters in c"); b = _b.default; }],
execute: function() { loadLog.push("body c"); _export('default', "exported from c"); }};
})
}
describe("sync loader", () => {
var S;
beforeEach(() => S = getSystem("lang-test"));
afterEach(() => removeSystem("lang-test"));
it("loads register-modules right away", () => {
var log = [];
defineEntries(S, log);
var exported = importSync(S, "a");
expect(exported.default).to.equal("exported from a, exported from b, exported from c");
expect(log.join(",")).to.equal('declared c,declared b,setters in b,setters in c,declared a,setters in a,body c,setters in b,body b,setters in c,setters in a,body a');
});
});
// TODO
// [ ] originalIndices
// [ ] esm modules
// [ ] nodejs native modules
// [ ] how to get source, especially for trace?
var __global = System.get("@system-env").node ?
global : (typeof window !== "undefined" ?
window : (typeof self !== "undefined" ? self : this))
import { graph, obj } from "lively.lang";
import { applyConfig as realApplyConfig } from "./packages.js";
import { getSystem, removeSystem } from "./system.js";
export { importSync, getSystem, removeSystem }
function importSync(System, moduleName) {
var id = System.normalizeSync(moduleName);
// if it's a package.json, load + install it into System object and apply
// it's settings to the System config
var configMatch = id.match(/(.*)package\.json(\.js)?$/);
if (configMatch) {
var cfg = loadPackageConfigFromDefined(System, id),
pkgURL = configMatch[1].replace(/\/$/, "");
applyConfig(System, cfg, pkgURL);
return cfg;
}
// Otherwise do a normal load of `moduleName`. Find and order the
// dependencies, then import (declare, link, instantiate, evaluate) the target
// module (which will load the dependencies in turn)
var baseURL = id.split("/").slice(0,-1).join("/"),
entries = getDefinedAsEntries(System, baseURL)
.map(entry => System.defined[entry.name] = entry);
if (!entries.length) {
throw new Error("Cannot find any registered modules for " + moduleName);
}
var missing = entriesWithMissingDeps(System, entries);
if (missing.length)
throw new Error(`Missing dependencies when loading ${baseURL}! ${JSON.stringify(missing, null, 2)}`)
var firstModule = entries.find(entry => entry.name === id) || entries[0],
sorted = sortedByDeps(System, entries, firstModule);
evaluateEntries(sorted, System);
return System.get(firstModule.name);
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// package.json / configuration related
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function loadPackageConfigFromDefined(System, packageConfigName) {
var configEntry = System.defined[packageConfigName];
if (!configEntry) {
configEntry = System.defined[packageConfigName + ".js"];
}
if (configEntry) {
var config = System.newModule(configEntry.execute());
System.set(packageConfigName, config);
}
return config;
}
function applyConfig(System, config, pkgURL) {
if (config.systemjs) {
System.packages[pkgURL] = obj.deepMerge(System.packages[pkgURL], config.systemjs);
}
realApplyConfig(System, config, pkgURL);
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// process System.defined entries
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function getDefinedAsEntries(System, pkgURL) {
return Object.keys(System.defined).map(name => {
if (name.indexOf(pkgURL) !== 0) return null;
if (name.match(/\.json(\.js)?$/)) return null;
var def = System.defined[name];
if (!def) return null; // alread loaded
var normalizedDeps = def.deps.map(dep =>
System.normalizeSync(dep, def.name))
var e = obj.deepMerge({deps: [], normalizedDeps: normalizedDeps, metadata: {}}, def);
e.esmExports = true; // ????
return e;
}).filter(ea => !!ea);
}
function entriesWithMissingDeps(System, entries) {
// returns [{name, dep}]
var entryNames = entries.pluck("name");
return entries.reduce((missing, ea) => {
var unresolved = ea.normalizedDeps.filter(dep =>
dep[0] !== "@" // not a "special" dep
&& entryNames.indexOf(dep) === -1 // not defined
&& !System.get(dep) // not loaded
&& !Object.keys(System.defined || {}).some(unloaded => unloaded === dep)
);
return missing.concat(unresolved.map(dep => ({name: ea.name, dep: dep})));
}, []);
}
function sortedByDeps(System, entries, mainEntry) {
if (!entries.length) return [];
var depGraph = entries.reduce((depGraph, ea) => {
depGraph[ea.name] = ea.normalizedDeps; return depGraph }, {}),
sorted = graph.sortByReference(depGraph, mainEntry.name).flatten(),
found = sorted.map(name => entries.find(e => e.name === name)).compact(),
notFound = entries.withoutAll(found);
if (notFound.length)
found = found.concat(sortedByDeps(notFound, notFound[0], System));
return found;
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// module loading / instantiation logic. Derived and compatible to systemjs 0.19.24
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function getModule(name, loader) {
// from SystemJS
// An analog to loader.get covering execution of all three layers (real declarative, simulated declarative, simulated dynamic)
var exports, entry = loader.defined[name];
if (!entry) {
exports = loader.get(name);
if (!exports)
throw new Error('Unable to load dependency ' + name + '.');
}
else {
if (entry.declarative) {
if (!entry.module) linkDeclarativeModule(entry, loader)
ensureEvaluated(name, [], loader);
}
else if (!entry.evaluated)
linkDynamicModule(entry, loader);
exports = entry.declarative && entry.esModule ? entry.esModule : entry.module.exports;
}
if ((!entry || entry.declarative) && exports && exports.__useDefault)
return exports['default'];
return exports;
}
function getESModule(exports) {
var esModule = {};
// don't trigger getters/setters in environments that support them
if (typeof exports == 'object' || typeof exports == 'function') {
if (Object.getOwnPropertyDescriptor) {
var d;
for (var p in exports)
if (d = Object.getOwnPropertyDescriptor(exports, p))
Object.defineProperty(esModule, p, d);
}
else {
var hasOwnProperty = exports && exports.hasOwnProperty;
for (var p in exports) {
if (!hasOwnProperty || exports.hasOwnProperty(p))
esModule[p] = exports[p];
}
}
}
esModule['default'] = exports;
Object.defineProperty(esModule, '__useDefault', {
value: true
});
return esModule;
}
function linkDynamicModule(entry, loader) {
// from systemjs
if (entry.module)
return;
var exports = {};
var module = entry.module = { exports: exports, id: entry.name };
// AMD requires execute the tree first
if (!entry.executingRequire) {
for (var i = 0, l = entry.normalizedDeps.length; i < l; i++) {
var depName = entry.normalizedDeps[i];
// we know we only need to link dynamic due to linking algorithm
var depEntry = loader.defined[depName];
if (depEntry)
linkDynamicModule(depEntry, loader);
}
}
// now execute
entry.evaluated = true;
var output = entry.execute.call(__global, function(name) {
for (var i = 0, l = entry.deps.length; i < l; i++) {
if (entry.deps[i] != name)
continue;
return getModule(entry.normalizedDeps[i], loader);
}
// try and normalize the dependency to see if we have another form
var nameNormalized = loader.normalizeSync(name, entry.name);
if (entry.normalizedDeps.indexOf(nameNormalized) != -1)
return getModule(nameNormalized, loader);
throw new Error('Module ' + name + ' not declared as a dependency of ' + entry.name);
}, exports, module);
if (output)
module.exports = output;
// create the esModule object, which allows ES6 named imports of dynamics
exports = module.exports;
// __esModule flag treats as already-named
var Module = System.get("@system-env").constructor;
if (exports && (exports.__esModule || exports instanceof Module))
entry.esModule = exports;
// set module as 'default' export, then fake named exports by iterating properties
else if (entry.esmExports && exports !== __global)
entry.esModule = getESModule(exports);
// just use the 'default' export
else
entry.esModule = { 'default': exports };
}
function getOrCreateModuleRecord(name, moduleRecords) {
return moduleRecords[name] || (moduleRecords[name] = {
name: name,
dependencies: [],
exports: {toString: () => "Module"},
importers: []
});
}
function linkDeclarativeModule(entry, loader) {
// only link if already not already started linking (stops at circular)
if (entry.module)
return;
var moduleRecords = loader._loader.moduleRecords;
var module = entry.module = getOrCreateModuleRecord(entry.name, moduleRecords);
var exports = entry.module.exports;
var declaration = entry.declare.call(__global, function(name, value) {
module.locked = true;
if (typeof name == 'object') {
for (var p in name)
exports[p] = name[p];
}
else {
exports[name] = value;
}
for (var i = 0, l = module.importers.length; i < l; i++) {
var importerModule = module.importers[i];
if (!importerModule.locked) {
var importerIndex = importerModule.dependencies.indexOf(module);
importerModule.setters[importerIndex](exports);
}
}
module.locked = false;
return value;
}, { id: entry.name });
module.setters = declaration.setters;
module.execute = declaration.execute;
if (!module.setters || !module.execute) {
throw new TypeError('Invalid System.register form for ' + entry.name);
}
// now link all the module dependencies
for (var i = 0, l = entry.normalizedDeps.length; i < l; i++) {
var depName = entry.normalizedDeps[i];
var depEntry = loader.defined[depName];
var depModule = moduleRecords[depName];
// work out how to set depExports based on scenarios...
var depExports;
if (depModule) {
depExports = depModule.exports;
}
// dynamic, already linked in our registry
else if (depEntry && !depEntry.declarative) {
depExports = depEntry.esModule;
}
// in the loader registry
else if (!depEntry) {
depExports = loader.get(depName);
}
// we have an entry -> link
else {
linkDeclarativeModule(depEntry, loader);
depModule = depEntry.module;
depExports = depModule.exports;
}
// only declarative modules have dynamic bindings
if (depModule && depModule.importers) {
depModule.importers.push(module);
module.dependencies.push(depModule);
}
else {
module.dependencies.push(null);
}
// run setters for all entries with the matching dependency name
module.setters[i](depExports);
// TODO!
// originalIndices are currently not set when creating entry / load!
// var depsForSetters = entry.normalizedDeps.map(dep => depExports);
// getModule
// if (depsForSetters.include(undefined)) debugger;
// depsForSetters.forEach((depExports, i) => module.setters[i](depExports));
// var originalIndices = entry.originalIndices[i];
// for (var j = 0, len = originalIndices.length; j < len; ++j) {
// var index = originalIndices[j];
// if (module.setters[index]) {
// module.setters[index](depExports);
// }
// }
}
}
function ensureEvaluated(moduleName, seen, loader) {
var entry = loader.defined[moduleName];
// if already seen, that means it's an already-evaluated non circular dependency
if (!entry || entry.evaluated || !entry.declarative)
return;
// this only applies to declarative modules which late-execute
seen.push(moduleName);
for (var i = 0, l = entry.normalizedDeps.length; i < l; i++) {
var depName = entry.normalizedDeps[i];
if (seen.indexOf(depName) == -1) {
if (!loader.defined[depName])
loader.get(depName);
else
ensureEvaluated(depName, seen, loader);
}
}
if (entry.evaluated)
return;
entry.evaluated = true;
// return evaluateEntries([entry], loader)[0];
if (!entry.module) show(entry.name)
entry.module.execute.call(__global);
}
function recordTrace(entry, System) {
if (!System.trace) return;
if (!System.loads) System.loads = {};
var depMap = entry.deps.reduce((depMap, dep, i) => {
depMap[dep] = entry.normalizedDeps[i];
return depMap;
}, {});
System.loads[entry.name] = {
name: entry.name,
deps: entry.deps.concat([]),
depMap: depMap,
address: entry.address || entry.name,
metadata: entry.metadata,
source: entry.source,
kind: entry.declarative ? 'declarative' : 'dynamic'
};
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Better support for sync. normalize
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function doMapWithObject(mappedObject, pkg, loader) {
// SystemJS allows stuff like {events: {"node": "@node/events", "~node": "@empty"}}
// for conditional name lookups based on the environment. The resolution
// process in SystemJS is asynchronous, this one here synch. to support
// normalizeSync and a one-step-load
var env = loader.get(pkg.map['@env'] || '@system-env');
// first map condition to match is used
var resolved;
for (var e in mappedObject) {
var negate = e[0] == '~';
var value = readMemberExpression(negate ? e.substr(1) : e, env);
if (!negate && value || negate && !value) {
resolved = mappedObject[e];
break;
}
}
if (resolved) {
if (typeof resolved != 'string')
throw new Error('Unable to map a package conditional to a package conditional.');
}
return resolved;
}
function readMemberExpression(p, value) {
var pParts = p.split('.');
while (pParts.length)
value = value[pParts.shift()];
return value;
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// sync. loading support
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function instantiateRegisteredEntry(entry, System) {
// instantiate : ({ name: NormalizedModuleName?,
// address: ModuleAddress?,
// source: ModuleSource,
// metadata: object })
// -> Promise<ModuleFactory?>
return {
deps: entry.deps,
execute: function() {
System.defined[entry.name] = undefined;
if (entry.declarative && entry.module.execute) entry.module.execute();
var module = System.newModule(entry.declarative ? entry.module.exports : entry.esModule);
System.set(entry.name, module);
recordTrace(entry, System);
return module;
}
}
}
function evaluateEntries(entries, System) {
// do link
// entries.map(entry => {
// if (entry.declarative)
// linkDeclarativeModule(entry, S)
// else {
// linkDynamicModule(entry, S)
// instantiateRegisteredEntry(entry, S).execute();
// }
// });
// return entries.map(entry => {
// if (entry.declarative)
// return instantiateRegisteredEntry(entry, S).execute();
// return System.get(entry.name);
// })
// do link
entries.forEach(entry =>
entry.declarative ?
linkDeclarativeModule(entry, System) :
linkDynamicModule(entry, System));
// ...and instantiate + execute
// execute will install loaded module in _loader.modules
return entries.map(entry =>
instantiateRegisteredEntry(entry, System).execute());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment