Runtime linking for ES2015 Modules [sic]
{"processors":{}}
// GIST: https://jsbin.com/gist/985fa2ef7681dfb48f7bce7b67fa0b18
// GIST: https://gist.github.com/SMotaal/985fa2ef7681dfb48f7bce7b67fa0b18
// LOCAL: https://jsbin.com/local/spring-night-2A7
(() => {
console.clear();
/** @type {typeof global & typeof window} */
const scope = this
|| typeof window === 'object' && window
|| typeof global === 'object' && global
|| typeof self === 'object' && self
|| {};
//Logging
const
ENTRIES = new class Entries extends Array { },
ERRORS = new class Errors extends Array { },
MARK = (OP, ID, value) =>
ENTRIES.push({ name: `«${ID}» — ${OP}`, value });
// Executors
const maybe = ƒ => { try { return ƒ() } catch (e) { } };
const noop = () => { };
// Formatters
const reindent = (
string, indent = (/\n?( *)$/.exec(string))[1].length
) => string.replace(new RegExp(`^ {0,${indent}}`, 'gm'), '');
(function (helpers) {
const {
runtime: { global, DynamicImport, RegisterModule },
performance: { START = noop, END = noop },
} = helpers;
const ModuleSources = global['[[ModuleSources]]'] = {};
(() => {
const origin = `x:/`;
const normalize = (specifier, referrer) => {
const base = referrer && (
referrer.includes(':/') && referrer || normalize(referrer, origin)
) || origin;
const reference = /^#|[.]{0,2}[/]|\w+:/.test(specifier)
? specifier.replace(/^[/]{2,}/, '/') : `/#${specifier}`;
return `${new URL(reference, base)}`;
}
const unnormalize = (url) => url.replace(/^x:(\/#|)/, '');
const ModuleLoader = global['[[ModuleLoader]]'] = {
resolve: (specifier, referrer) =>
unnormalize(normalize(specifier, referrer)),
load: async (specifier, referrer) => {
const sourceURL = ModuleLoader.resolve(specifier, referrer)
if (sourceURL in ModuleMap)
return ModuleMap[sourceURL];
if (sourceURL in ModuleSources)
return ModuleMap.define(ModuleSources[sourceURL], specifier, referrer);
throw `Failed to load module from "${sourceURL}"`;
},
import: async (specifier, referrer) => {
return (await ModuleLoader.load(specifier, referrer)).namespace();
}
};
const ValidModuleSpecifiers = /^((import(?= +\* +as \w+| +(?:\w+, *)?\{[\w\s,]*?\}| +\w+)|export(?= +\*| +\{[\w\s,]*?\})) +(.*?) +from +(['"]))(.*?)(\4)/gmsu;
class Module {
constructor(properties) {
Object.assign(this, properties);
}
async link() {
const sourceURL = this.sourceURL;
const links = this.links;
START('[3] link', sourceURL);
let linkedSource = this.prelinkedSource;
for (const link of links) {
const requiredModule = ModuleMap[link.url];
linkedSource = linkedSource.replace(link.marker, `${link.head}${
requiredModule
&& (
requiredModule.url || (await requiredModule.link())
) || await (
(await ModuleLoader.load(link.specifier, sourceURL)).link()
) || link.specifier
}${link.tail}`.padEnd(link.declaration.length));
}
const url = this.url = RegisterModule(this.linkedSource = linkedSource, sourceURL);
ModuleMap[sourceURL] = ModuleMap[url] = this;
END('[3] link', sourceURL);
return url;
}
async instantiate() {
const sourceURL = this.sourceURL;
const url = this.url || await this.link();
START('[4] instantiate', sourceURL);
try {
await (this.namespace = () => DynamicImport(url))();
} catch (exception) {
this.error = exception;
}
END('[4] instantiate', sourceURL);
return ModuleMap[sourceURL] = ModuleMap[url] = this;
}
async namespace() {
return (await this.instantiate()).namespace();
}
}
const ModuleMap = global['[[ModuleMap]]'] = {
define: (source, specifier, referrer = origin) => {
// const mappedURL = normalize(specifier, referrer);
// const sourceURL = unnormalize(mappedURL);
const sourceURL = ModuleLoader.resolve(specifier, referrer);
const baseURL = /^\w/.test(sourceURL) ? origin : sourceURL;
if (sourceURL in ModuleMap)
console.warn(`\n\nRedefining "${sourceURL}"!\n\n`);
START('[1] define', sourceURL);
const links = [];
START('[2] prelink', sourceURL);
const prelinkedSource = source.replace(ValidModuleSpecifiers, (
declaration, head, type, bindings, quote, specifier, tail
) => {
const marker = `««—${links.length}—${specifier}—»»`;
links.push({
declaration, head, type, bindings, quote, specifier, tail, marker,
url: ModuleLoader.resolve(specifier, baseURL)
});
return marker;
});
END('[2] prelink', sourceURL);
const module = new Module({
source, sourceURL, links, prelinkedSource
});
END('[1] define', sourceURL);
return ModuleMap[sourceURL] = module;
},
}
return ModuleSources;
})();
setTimeout(() => test({ global, timeline: ENTRIES }), 100);
})({
runtime: RuntimeHelpers(),
performance: PerformanceHelpers(),
});
function RuntimeHelpers() {
const
JS = 'text/javascript',
UID = () => ~~(1e6 * Math.random()),
SRC = (uid = UID()) =>
`VirtualModule-${uid}`,
SourceText = (source, url = SRC()) =>
`${source}\n\n//# sourceURL=${url}\n`;
if (typeof window !== 'undefined' && (
typeof process === 'undefined'
|| process.__nwjs
)) {
const importModule = async (url) => eval(`import("${url}")`);
const
SourceFile = (source, url = SRC(), type = JS) =>
new File([SourceText(source, url)], url, { type }),
SourceFileURL = (source, url = SRC(), type = JS) =>
URL.createObjectURL(SourceFile(source, url, type)),
RegisterModule = (source, sourceURL) =>
SourceFileURL(SourceText(source, sourceURL)),
DynamicImport = (url) => importModule(url);
return { global: window, RegisterModule, DynamicImport };
}
if (typeof process !== 'undefined') {
global.URL || (global.URL = require('url').URL);
// const moduleHelpers = require('./node-vm-module-helpers');
// const moduleHelpers = require('./node-module-wrap-helpers');
const moduleHelpers = require('/Users/daflair/Projects/imaging-js/packages/kernel/node-module-wrap-helpers.js');
moduleHelpers.global = global;
return moduleHelpers;
}
throw Error('Unsupported runtime');
}
function PerformanceHelpers() {
const { performance, PerformanceObserver } =
(scope.PerformanceObserver && scope.performance)
&& (!scope.process || !scope.process.moduleLoadList)
&& scope || maybe(() => require('perf_hooks')) || {};
performance && PerformanceObserver && new PerformanceObserver(
(list, observer) => ENTRIES.push(...list.getEntries())
).observe({ entryTypes: ['measure'] }); // console.log({ performance, scope })
return performance ? {
performance, ENTRIES, MARK,
START: (OP, ID) =>
performance.mark(`START: «${ID}» — ${OP}`),
END: (OP, ID) =>
performance.measure(
`«${ID}» — ${OP}`, `START: «${ID}» — ${OP}`, (
performance.mark(`END: «${ID}» — ${OP}`),
`END: «${ID}» — ${OP}`
)),
} : { performance, ENTRIES, MARK };
}
async function test({
global,
ModuleLoader = global['[[ModuleLoader]]'],
ModuleSources = global['[[ModuleSources]]'],
records = new class Records { },
namespaces = new class Namespaces { },
timeline = [],
sources = {
['/modules/typescript']: reindent(`
export const typescript = true;
export default "TypeScript"
`),
['typescript']: reindent(`
export * from '/modules/typescript';
export {\ndefault\n} from '/modules/typescript';
`),
['/index']: reindent(`
import ts from 'typescript'; // import ts, {a, b, c} from 'typescript';
export * from 'typescript';
export const index = true;
export { ts };
`),
['/tests/works']: reindent(`
import * as ts1 from '../modules/typescript';
import * as ts2 from '//modules/x/../typescript';
import * as ts3 from 'typescript';
console.log('/tests/works', {
'/modules/typescript': ts1,
'//modules/x/../typescript': ts2,
'typescript': ts3
});
`),
['/tests/async/resolve']: reindent(`
export function then(resolve, reject) {
try { resolve([1, 2, 3, 'Resolved Module']) }
catch (e) { console.warn('Thenable module not resolved!') }
}
`),
['/tests/async/reject']: reindent(`
export function then(resolve, reject) {
try { reject('Rejected Module') }
catch (e) { console.warn('Thenable module not rejected!') }
}
`),
['/tests/fails']: `import x from 'xyz';`,
['https://unpkg.com/lit-html@0.9.0/lit-html.js']: `export {};`,
['lit-html']: `export * from 'https://unpkg.com/lit-html@0.9.0/lit-html.js';`,
['/tests/deadlock/a']: `export * from './b';\nexport default 'A';\nconsole.log('deadlock a');`,
['/tests/deadlock/b']: `export * from './a';\nexport default 'B';\nconsole.log('deadlock b');`,
},
specifiers = Object.keys(sources),
}) {
Object.assign(ModuleSources, sources);
for (const specifier of specifiers) {
try {
records[specifier] = await ModuleLoader.load(specifier);
namespaces[specifier] = await ModuleLoader.import(specifier);
} catch (exception) {
namespaces[specifier] = exception;
}
}
console.dir(timeline.reduce(
(entries, { name, duration, value }) => ((
entries[name] = value || duration || true
), entries), {
'[RECORDS]': records,
'[NAMESPACES]': namespaces
}
));
}
})()
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
</body>
</html>
This prototype simply relies on async execution to walk the module graph and uses the platform to create either a ModuleWrap in node (when possible) or a blob which is used to determine the actual specifier to use for each link outwards.
A given limitation is that circular references will result in deadlock.