36
loading...
This website collects cookies to deliver better user experience
import('https://cdn.skypack.dev/uuid')
?node --experimental-loader ./https-loader.mjs
import fetch from 'node-fetch';
async function fetchCode(url) {
const response = await fetch(url);
if (response.ok) {
return response.text();
} else {
throw new Error(
`Error fetching ${url}: ${response.statusText}`
);
}
const url = 'import cdn.skypack.dev/lodash-es';
const source = await fetchCode(url);
eval
but more sophisticated. However, this API only works with the classic CommonJS modules.--experimental-vm-modules
flag.vm.Module
we are going to implement the 3 distinct steps creation/parsing, linking, and evaluation:import vm from 'vm';
const context = vm.createContext({});
vm.SourceTextModule
which is a subclass of vm.Module
specifically for raw source code strings.return new vm.SourceTextModule(source, {
identifier: url,
context,
});
identifier
is the name of the module. We set it to the original HTTP URL because we are going to need it for resolving additional imports in the next step.import
statements in the code, we must implement a custom link
function. This function should return a new vm.SourceTextModule
instance for the two arguments it receives:"lodash-es"
.vm.Module
and the "parent" module of the imported dependency.async function link(specifier, referencingModule) {
// Create a new absolute URL from the imported
// module's URL (specifier) and the parent module's
// URL (referencingModule.identifier).
const url = new URL(
specifier,
referencingModule.identifier,
).toString();
// Download the raw source code.
const source = await fetchCode(url);
// Instantiate a new module and return it.
return new vm.SourceTextModule(source, {
identifier: url,
context: referencingModule.context
});
}
await mod.link(link); // Perform the "link" step.
link
step, the original module instance is fully initialised and any exports could already be extracted from its namespace. However, if there are any imperative statements in the code that should be executed, this additional step is necessary.await mod.evaluate(); // Executes any imperative code.
// The following corresponds to
// import { random } from 'https://cdn.skypack.dev/lodash-es';
const { random } = mod.namespace;
crypto
, which is the Web Crypto API. Node.js provides an implementation of this API since version 15 and we can inject it into the context as a global variable.import { webcrypto } from 'crypto';
import vm from 'vm';
const context = vm.createContext({ crypto: webcrypto });
process
.require
statement of CommonJS would look like when importing a module from node_modules
.import uuid from 'uuid'; // Where does 'uuid' come from?
{
"imports": {
"uuid": "https://www.skypack.dev/view/uuid"
}
}
link
function that is used by SourceTextModule
to resolve additional imports could be updated to look up entries in the map:const { imports } = importMap;
const url =
specifier in imports
? imports[specifier]
: new URL(specifier, referencingModule.identifier).toString();
fs
?link
function to detect wether an import is for a Node.js builtin module. One possibility would be to look up the specifier in the list of builtin module names.import { builtinModules } from 'module';
// Is the specifier, e.g. "fs", for a builtin module?
if (builtinModules.includes(specifier)) {
// Create a vm.Module for a Node.js builtin module
}
node:
URL protocol. In fact, Node.js ECMAScript modules already support node:
, file:
and data:
protocols for their import statements (and we just added support for http/s:
).// An import map with an entry for "fs"
const { imports } = {
imports: { fs: 'node:fs/promises' }
};
const url =
specifier in imports
? new URL(imports[specifier])
: new URL(specifier);
if (
url.protocol === 'http:' ||
url.protocol === 'https:'
) {
// Download code and create a vm.SourceTextModule
} else if (url.protocol === 'node:') {
// Create a vm.Module for a Node.js builtin module.
} else {
// Other possible schemes could be file: and data:
}
vm.Module
for a Node.js builtin module? If we used another SourceTextModule with an export
statement for, e.g. fs
, it would lead to an endlessly recursive loop of calling the link
function over and over again. export default fs
, where fs
is a global variable on the context, the exported module would be wrapped inside an object with the default
property.// This leads to an endless loop, calling the "link" function.
new vm.SourceTextModule(`export * from 'fs';`);
// This ends up as an object like { default: {...} }
new vm.SourceTextModule(`export default fs;`, {
context: { fs: await import('fs') }
});
vm.Module
allows us to programatically construct a module without a source code string.// Actually import the Node.js builtin module
const imported = await import(identifier);
const exportNames = Object.keys(imported);
// Construct a new module from the actual import
return new vm.SyntheticModule(
exportNames,
function () {
for (const name of exportNames) {
this.setExport(name, imported[name]);
}
},
{
identifier,
context: referencingModule.context
}
);
vm.Module
were used in this blog post, vm.Script
could be used to implement a similar solution for CommonJS modules.import
statements. On the other hand, they are less flexible and they're possibly even more experimental than vm.Module
.Skypack is nice because it offers ESM versions of most npm packages. ↩