Is Deno's dynamic import() implementation conformant with ECMA-262?

124 Views Asked by At

Specification:

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");
  }
}
0

There are 0 best solutions below