Last active
January 15, 2018 05:09
-
-
Save TimothyGu/3c5285d554dd1f8c333685475ad01613 to your computer and use it in GitHub Desktop.
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
"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