Repository: vercel/nft (javascript)

Issue:
Does not include registered ESM loader hooks
`package.json`
```json
{
  "type": "module",
  "dependencies": {
    "import-in-the-middle": "^1.8.1"
  },
  "devDependencies": {
    "@vercel/nft": "^0.27.2"
  }
}
```
`test.js`
```ts
import { register } from 'module';

register('import-in-the-middle/hook.mjs', import.meta.url);
```
`build.js`
```ts
import { nodeFileTrace } from '@vercel/nft';
const { fileList } = await nodeFileTrace(['./test.js']);
console.log(fileList)
```

Running `> node build.js`
Outputs `Set(1) { 'test.js' }`

But `import-in-the-middle/hook.mjs` and all of it's dependencies should be included.


Discussion:

Patch:
diff --git a/src/analyze.ts b/src/analyze.ts
--- a/src/analyze.ts
+++ b/src/analyze.ts
@@ -37,6 +37,7 @@ const acorn = Parser.extend(
 );
 
 import os from 'os';
+import url from 'url';
 import { handleWrappers } from './utils/wrappers';
 import resolveFrom from 'resolve-from';
 import {
@@ -94,6 +95,10 @@ const fsExtraSymbols = {
   readJsonSync: FS_FN,
   readJSONSync: FS_FN,
 };
+const MODULE_FN = Symbol();
+const moduleSymbols = {
+  register: MODULE_FN,
+};
 const staticModules = Object.assign(Object.create(null), {
   bindings: {
     default: BINDINGS,
@@ -111,6 +116,10 @@ const staticModules = Object.assign(Object.create(null), {
     default: fsSymbols,
     ...fsSymbols,
   },
+  module: {
+    default: moduleSymbols,
+    ...moduleSymbols,
+  },
   'fs-extra': {
     default: fsExtraSymbols,
     ...fsExtraSymbols,
@@ -131,6 +140,10 @@ const staticModules = Object.assign(Object.create(null), {
     default: os,
     ...os,
   },
+  url: {
+    default: url,
+    ...url,
+  },
   '@mapbox/node-pre-gyp': {
     default: mapboxPregyp,
     ...mapboxPregyp,
@@ -530,15 +543,19 @@ export default async function analyze(
     let computed = await computePureStaticValue(expression, true);
     if (!computed) return;
 
+    function add(value: string) {
+      (isImport ? imports : deps).add(value);
+    }
+
     if ('value' in computed && typeof computed.value === 'string') {
-      if (!computed.wildcards) (isImport ? imports : deps).add(computed.value);
+      if (!computed.wildcards) add(computed.value);
       else if (computed.wildcards.length >= 1)
         emitWildcardRequire(computed.value);
     } else {
       if ('then' in computed && typeof computed.then === 'string')
-        (isImport ? imports : deps).add(computed.then);
+        add(computed.then);
       if ('else' in computed && typeof computed.else === 'string')
-        (isImport ? imports : deps).add(computed.else);
+        add(computed.else);
     }
   }
 
@@ -876,6 +893,52 @@ export default async function analyze(
                 pjsonPath = path.resolve(pjsonPath, '../../package.json');
               if (pjsonPath !== rootPjson) assets.add(pjsonPath);
               break;
+            case MODULE_FN:
+              if (
+                node.arguments.length &&
+                // TODO: We only cater for the case where the first argument is a string
+                node.arguments[0].type === 'Literal'
+              ) {
+                const pathOrSpecifier = node.arguments[0].value;
+                // It's a relative URL
+                if (pathOrSpecifier.startsWith('.')) {
+                  // Compute the parentURL if it's statically analyzable
+                  const computedParentURL =
+                    node.arguments.length > 1
+                      ? await computePureStaticValue(node.arguments[1])
+                      : undefined;
+
+                  if (computedParentURL && 'value' in computedParentURL) {
+                    const parentURL =
+                      computedParentURL.value instanceof URL
+                        ? computedParentURL.value.href
+                        : typeof computedParentURL.value === 'string'
+                          ? computedParentURL.value
+                          : computedParentURL.value.parentURL;
+
+                    // Resolve the srcURL from the parentURL
+                    const srcURL = new URL(pathOrSpecifier, parentURL).href;
+
+                    const currentDirURL = importMetaUrl.slice(
+                      0,
+                      importMetaUrl.lastIndexOf('/'),
+                    );
+
+                    // Resolve the srcPath relative to the current file
+                    const srcPath = path.relative(currentDirURL, srcURL);
+                    // Make it a proper relative path
+                    const relativeSrcPath = srcPath.startsWith('.')
+                      ? srcPath
+                      : './' + srcPath;
+
+                    imports.add(relativeSrcPath);
+                  }
+                } else {
+                  // It's a bare specifier, so just add into the imports
+                  imports.add(pathOrSpecifier);
+                }
+              }
+              break;
           }
         }
       } else if (


Test patch:
diff --git a/test/unit/module-register/hook.mjs b/test/unit/module-register/hook.mjs
new file mode 100644
--- /dev/null
+++ b/test/unit/module-register/hook.mjs
@@ -0,0 +1 @@
+console.log('hook.mjs');
\ No newline at end of file
diff --git a/test/unit/module-register/hook2.mjs b/test/unit/module-register/hook2.mjs
new file mode 100644
--- /dev/null
+++ b/test/unit/module-register/hook2.mjs
@@ -0,0 +1 @@
+console.log('hook2.mjs');
\ No newline at end of file
diff --git a/test/unit/module-register/hook3.mjs b/test/unit/module-register/hook3.mjs
new file mode 100644
--- /dev/null
+++ b/test/unit/module-register/hook3.mjs
@@ -0,0 +1 @@
+console.log('hook3.mjs');
\ No newline at end of file
diff --git a/test/unit/module-register/input-esm.mjs b/test/unit/module-register/input-esm.mjs
new file mode 100644
--- /dev/null
+++ b/test/unit/module-register/input-esm.mjs
@@ -0,0 +1,8 @@
+import { register } from 'module';
+
+// Load relative to the current file
+register('./hook.mjs', import.meta.url);
+// Load from a bare specifier
+register('test-pkg');
+// Load with parentURL in options object
+register('./hook.mjs', { parentURL: import.meta.url });
\ No newline at end of file
diff --git a/test/unit/module-register/input.js b/test/unit/module-register/input.js
new file mode 100644
--- /dev/null
+++ b/test/unit/module-register/input.js
@@ -0,0 +1,10 @@
+const { register } = require('module');
+const { pathToFileURL } = require('url');
+
+import('./input-esm.mjs')
+
+// Load relative to the current file
+register('./hook2.mjs', pathToFileURL(__filename));
+// Load relative to the current working directory
+register('./test/unit/module-register/hook3.mjs', pathToFileURL('./'));
+
diff --git a/test/unit/module-register/node_modules/test-pkg/index.mjs b/test/unit/module-register/node_modules/test-pkg/index.mjs
new file mode 100644
--- /dev/null
+++ b/test/unit/module-register/node_modules/test-pkg/index.mjs
@@ -0,0 +1 @@
+console.log('test-pkg/index.mjs');
\ No newline at end of file
diff --git a/test/unit/module-register/node_modules/test-pkg/package.json b/test/unit/module-register/node_modules/test-pkg/package.json
new file mode 100644
--- /dev/null
+++ b/test/unit/module-register/node_modules/test-pkg/package.json
@@ -0,0 +1,7 @@
+{
+    "name": "test-pkg",
+    "main": "./index.mjs",
+    "exports":{
+        ".": "./index.mjs"
+    }
+}
\ No newline at end of file
diff --git a/test/unit/module-register/output.js b/test/unit/module-register/output.js
new file mode 100644
--- /dev/null
+++ b/test/unit/module-register/output.js
@@ -0,0 +1,10 @@
+[
+  "package.json",
+  "test/unit/module-register/hook.mjs",
+  "test/unit/module-register/hook2.mjs",
+  "test/unit/module-register/hook3.mjs",
+  "test/unit/module-register/input-esm.mjs",
+  "test/unit/module-register/input.js",
+  "test/unit/module-register/node_modules/test-pkg/index.mjs",
+  "test/unit/module-register/node_modules/test-pkg/package.json"
+]