Skip to content

Instantly share code, notes, and snippets.

@TimothyGu
Last active January 15, 2018 05:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TimothyGu/3c5285d554dd1f8c333685475ad01613 to your computer and use it in GitHub Desktop.
Save TimothyGu/3c5285d554dd1f8c333685475ad01613 to your computer and use it in GitHub Desktop.
"use strict";
// Example integration of the new vm.Module API into jsdom.
const vm = require("vm");
const { URL } = require("url");
class ModuleScript {
constructor() {
this.context = null;
this.module = null;
this.parseError = null;
this.errorToRethrow = null;
this.fetchOptions = null;
this.baseURL = undefined;
}
}
const hostDefinedMap = new WeakMap();
// Based on https://html.spec.whatwg.org/multipage/webappapis.html#hostresolveimportedmodule(referencingscriptormodule,-specifier)
async function linker(referencingModule, specifier) {
const referencingModuleScript = hostDefinedMap.get(referencingModule);
const moduleMap = referencingModuleScript.context._moduleMap;
const url = resolveModuleSpecifier(referencingModuleScript, specifier);
const childMod = await moduleMap.get(url);
if (childMod.module.linkingStatus === "unlinked") {
await childMod.module.link(linker);
}
return childMod.module;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-module-script-tree
function fetchModuleScriptGraph(url, window, destination, options) {
const visitedSet = new Set([url]);
return internalModuleScriptGraphFetch(url, {
fetchClientWindow: window,
destination,
options,
moduleMapWindow: window,
visitedSet,
referrer: "client",
topLevel: true
});
}
// https://html.spec.whatwg.org/multipage/webappapis.html#internal-module-script-graph-fetching-procedure
// Throws an error instead of return null.
async function internalModuleScriptGraphFetch(url, {
fetchClientWindow,
destination,
options,
moduleMapWindow,
visitedSet,
referrer,
topLevel
}) {
const result = await fetchSingleModuleScript(url, {
fetchClientWindow,
destination,
options,
moduleMapWindow,
referrer,
topLevel
});
return (topLevel ? fetchDescendentsAndInstantiateModuleScript : fetchDescendentsModuleScript)(result, destination, visitedSet);
}
async function fetchSingleModuleScriptInner(url, { fetchClientWindow, destination, options, moduleMapWindow, referrer, topLevel }) {
const response = await fetch(url, { destination, referrer, client: fetchClientWindow }, options);
if (response.type === "error" || !response.ok || !isJavaScriptMIME(response)) {
throw new Error();
}
const sourceText = await response.text();
return createModuleScript(sourceText, moduleMapWindow, response.url, options);
}
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script
// Throws an error instead of return null.
async function fetchSingleModuleScript(url, { fetchClientWindow, destination, options, moduleMapWindow, referrer, topLevel }) {
const moduleMap = moduleMapWindow._moduleMap;
if (moduleMap.has(url)) {
return moduleMap.get(url);
}
const promise = fetchSingleModuleScriptInner(...arguments);
moduleMap.set(url, promise);
return promise;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#descendant-script-fetch-options
function descendentScriptFetchOptions(fetchOptions) {
return Object.assign({}, fetchOptions, {
integrityMetadata: ""
});
}
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-the-descendants-of-a-module-script
// Throws an error instead of return null.
async function fetchDescendentsModuleScript(moduleScript, destination, visitedSet) {
const { module } = moduleScript;
if (module === null) {
return moduleScript;
}
const urls = [];
for (const requested of module.dependencySpecifiers) {
const url = resolveModuleSpecifier(moduleScript, requested);
if (!visitedSet.has(url)) {
urls.push(url);
visitedSet.add(url);
}
}
const options = descendentScriptFetchOptions(moduleScript.fetchOptions);
const promises = [];
for (const url of urls) {
promises.push(internalModuleScriptGraphFetch(url, {
fetchClientWindow: moduleScript.context,
destination,
options,
moduleMapWindow: moduleScript.context,
visitedSet,
referrer: moduleScript.baseURL,
topLevel: false
}));
}
await Promise.all(promises);
return moduleScript;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-the-descendants-of-and-instantiate-a-module-script
// Throws an error instead of return null.
async function fetchDescendentsAndInstantiateModuleScript(moduleScript, destination, visitedSet = new Set()) {
const result = await fetchDescendentsModuleScript(moduleScript, destination, visitedSet);
const parseError = await findFirstParseError(result);
if (parseError === null) {
await result.module.link(linker);
try {
result.module.instantiate();
} catch (err) {
result.errorToRethrow = err;
}
} else {
result.errorToRethrow = parseError;
}
return result;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#finding-the-first-parse-error
async function findFirstParseError(moduleScript, discoveredSet = new Set()) {
const moduleMap = moduleScript.context._moduleMap;
discoveredSet.add(moduleScript);
if (moduleScript.module === null) {
return moduleScript.parseError;
}
const childSpecifiers = moduleScript.module.dependencySpecifiers;
const childURLs = childSpecifiers.map(specifier => resolveModuleSpecifier(moduleScript, specifier));
for (const childURL of childURLs) {
if (moduleMap.has(childURL)) {
const childModule = await moduleMap.get(childURL);
if (discoveredSet.has(childModule)) continue;
const childParseError = await findFirstParseError(childModule, discoveredSet);
if (childParseError !== null) {
return childParseError;
}
}
}
return null;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#creating-a-module-script
function createModuleScript(source, window, baseURL, options) {
const script = new ModuleScript();
script.context = window;
script.baseURL = baseURL; // https://github.com/whatwg/html/pull/3352
script.fetchOptions = options;
let result;
try {
result = new vm.Module(source, { context: window, url: baseURL });
} catch (err) {
script.parseError = err;
return script;
}
for (const requested of result.dependencySpecifiers) {
let url = resolveModuleSpecifier(script, requested);
if (url === null) {
script.parseError = new TypeError("URL parsing failure");
return script;
}
}
hostDefinedMap.set(result, script);
script.module = result;
return script;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#run-a-module-script
async function runModuleScript(script, rethrowErrors = false) {
if (script.errorToRethrow !== null) {
throw script.errorToRethrow;
}
return script.module.evaluate();
}
// https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier
function resolveModuleSpecifier(referrerModule, specifier) {
try {
const absoluteURL = new URL(specifier);
return absoluteURL.href;
} catch (e) {
if (!specifier.startsWith("/") && !specifier.startsWith("./") && !specifier.startsWith("../")) {
throw new TypeError("Bad module specifier");
}
return (new URL(specifier, referrerModule.baseURL)).href;
}
}
//////////////////////////////////////////////
// Code below are testing code.
const window = vm.createContext({
// Map of Promises that may eventually resolve to a Module
// in spec: module script; here: fulfilled promise
// null ; rejected promise
// "fetching" ; unresolved promise
_moduleMap: new Map(),
console: {
log: console.log
}
});
const FILES = {
"a.mjs":
`import def, { b } from "./b.mjs";
console.log(def);
console.log(b);
`,
"b.mjs":
`import { c } from "./c.mjs";
export let b = 120;
export default c + 4;
`,
"c.mjs":
`import { b } from "./b.mjs";
export let c = 110;
`
};
async function fetch(url, { referrer }) {
console.log(`fetching ${url} from ${referrer}`);
if (url.endsWith("error.mjs")) {
return { type: "error", ok: false };
}
return {
type: "ok",
ok: true,
url,
async text() {
return FILES[url.split(/\//g).slice(-1)[0]];
}
};
}
function isJavaScriptMIME() {
return true;
}
async function init() {
let script = createModuleScript(FILES["a.mjs"], window, "file:///a.mjs", {});
await fetchDescendentsAndInstantiateModuleScript(script, "script");
await runModuleScript(script);
}
init().catch(err => {
process.nextTick(() => { throw err; });
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment