Specification:
- 13.3.10.1 Runtime Semantics: Evaluation
- 13.3.10.1.1 ContinueDynamicImport ( promiseCapability, moduleCompletion )
When using dynamic import() in Deno there is a strange behaviour that is exclusive to Deno: bare string specifiers don't dynamically import the module and can be fashioned to consistently throw, see Deno dynamic import("./exports") throws module not found for "exports.js" dynamically created in the script.
After some research I located this Dynamic import module is not found when created after the application started. #20945 which while still open is marked as working as designed per Do not permission prompt for statically analyzable dynamic imports.
In the linked blog post https://deno.com/blog/v1.33#fewer-permission-checks-for-dynamic-imports we read
Keep in mind that permissions will still be checked for dynamic imports that are not statically analyzable (ie. don’t use string literals for the specifier):
import("" + "https://deno.land/std/version.ts");
import(`https://deno.land/std@${STD_VERSION}/version.ts`);
const someVariable = "./my_mod.ts";
import(someVariable);
This means we have to employ special treatment for ECMA-262 dynamic import() in Deno.
So, let's test with a variable that is not a raw string that satisfies that Deno-specific special treatment of specifiers when using import() and see what happens
// test_dynamic_module.js
import { exists } from "https://deno.land/std/fs/mod.ts";
const [...modules] = Deno.args;
console.log({ modules });
if (await exists("node_modules")) {
await Deno.remove("node_modules", {recursive: true});
}
for (const module of modules) {
try {
await import(module);
} catch (e) {
console.log(e);
}
}
deno run -A test_dynamic_import.js npm:zod
{ modules: [ "npm:zod" ] }
TypeError: Loading unprepared module: npm:zod, imported from: file:///home/user/test_dynamic_import.js
at async file:///home/user/test_dynamic_import.js:15:5 {
code: "ERR_MODULE_NOT_FOUND"
}
We get TypeError: Loading unprepared module.
But why should a dynamic import() have to be prepared?
If we use static import things work as intended.
To me this is a bug that is asserted to be working as designed though effectively means dynamic import() in Deno is not dynamic at all.
Is Deno's dynamic import() implementation conformant with ECMA-262?
-
To avoid speculation about what is going on here, we create the file exports.js on the local file system within the script dynamic_import_always_throws.js and test the same code using node, bun, and deno.
The file exists.
Only deno throws in this case of the raw string specifier.
deno consistently throws TypeError: Loading unprepared module code: ... "ERR_MODULE_NOT_FOUND" when the raw string specifier ./exports.js is used here const { default: module } = await import("./exports.js"); // Raw string specifier
// Deno dynamic import("./exports") throws module not found for "exports.js" dynamically created in the script
// dynamic_import_always_throws.js
// References: https://www.reddit.com/r/Deno/comments/18unb03/comment/kfsszsw/ https://github.com/denoland/deno/issues/20945
// Usage:
// deno run -A dynamic_import_always_throws.js
// bun run dynamic_import_always_throws.js
// node --experimental-default-type=module dynamic_import_always_throws.js
import { open, unlink } from "node:fs/promises";
const runtime = navigator.userAgent;
const encoder = new TextEncoder();
try {
const script = `export default 1;`;
// deno
if (runtime.includes("Deno")) {
await Deno.writeFile("exports.js", encoder.encode(script));
}
// node
if (runtime.includes("Node")) {
const dynamic = await open("./exports.js", "w");
await dynamic.write(script);
await dynamic.close();
}
// bun
if (runtime.includes("Bun")) {
await Bun.write("exports.js", encoder.encode(script));
}
const { default: module } = await import("./exports.js"); // Raw string specifier
console.log({ module });
console.log({ runtime });
} catch (e) {
console.log("Deno always throws.");
console.log({ runtime });
console.trace();
console.log(e.stack);
} finally {
console.log("Finally");
// node, bun
if (runtime.includes("Node") || runtime.includes("Bun")) {
await unlink("./exports.js");
} // deno
else if (runtime.includes("Deno")) {
await Deno.remove("./exports.js");
}
}