Repository: parcel-bundler/parcel (js)

Issue:
Support "external" modules in Parcel 2
Parcel could allow listing of "external" modules which won't be bundled.

Pull request:
Add globals support
<!---
Thanks for filing a pull request 😄 ! Before you submit, please read the following:

Search open/closed issues before submitting since someone might have pushed the same thing before!
-->

# ↪️ Pull Request

<!---
Provide a general summary of the pull request here
Please look for any issues that this PR resolves and tag them in the PR.
-->

Adds support for globals using the `alias` in `package.json`.

Based off of #4846 and #4850.

Fixes #3305.

## 💻 Examples

<!-- Examples help us understand the requested feature better -->
In `package.json`:
```json
{
    "alias": {
        "react": { "global": "React" },
        "myAliasedPackageName": { "fileName": "./myPkg.js" },
        "finalPackage": "finalPackage.js"
    }
}
```
All of the above syntax forms work, for globals and normal aliases respectively. <strike>The pure string syntax works less reliably because it has to guess that it's not an alias based on whether or not the file exists.</strike> (Removed)


## ✔️ PR Todo

- [x] Added/updated unit tests for this change
- [x] Included links to related issues/PRs


Patch:
diff --git a/packages/core/core/src/requests/PathRequest.js b/packages/core/core/src/requests/PathRequest.js
index 27ef8da5c..65b33fe28 100644
--- a/packages/core/core/src/requests/PathRequest.js
+++ b/packages/core/core/src/requests/PathRequest.js
@@ -158,19 +158,21 @@ export class ResolverRunner {
           logger: new PluginLogger({origin: resolver.name}),
         });
 
-        if (result && result.isExcluded) {
-          return null;
-        }
-
-        if (result?.filePath != null) {
-          return {
-            filePath: result.filePath,
-            sideEffects: result.sideEffects,
-            code: result.code,
-            env: dependency.env,
-            pipeline: pipeline ?? dependency.pipeline,
-            isURL: dependency.isURL,
-          };
+        if (result) {
+          if (result.isExcluded) {
+            return null;
+          }
+
+          if (result.filePath != null) {
+            return {
+              filePath: result.filePath,
+              sideEffects: result.sideEffects,
+              code: result.code,
+              env: dependency.env,
+              pipeline: pipeline ?? dependency.pipeline,
+              isURL: dependency.isURL,
+            };
+          }
         }
       } catch (e) {
         // Add error to error map, we'll append these to the standard error if we can't resolve the asset
diff --git a/packages/utils/node-resolver-core/src/NodeResolver.js b/packages/utils/node-resolver-core/src/NodeResolver.js
index f5d72370f..5e16b5c58 100644
--- a/packages/utils/node-resolver-core/src/NodeResolver.js
+++ b/packages/utils/node-resolver-core/src/NodeResolver.js
@@ -46,6 +46,7 @@ type Module = {|
   subPath?: string,
   moduleDir?: FilePath,
   filePath?: FilePath,
+  code?: string,
 |};
 
 /**
@@ -110,6 +111,12 @@ export default class NodeResolver {
     if (module.moduleDir) {
       resolved = await this.loadNodeModules(module, extensions, env);
     } else if (module.filePath) {
+      if (module.code != null) {
+        return {
+          filePath: module.filePath,
+          code: module.code,
+        };
+      }
       resolved = await this.loadRelative(
         module.filePath,
         extensions,
@@ -266,7 +273,18 @@ export default class NodeResolver {
     }
 
     // Resolve aliases in the parent module for this file.
-    filename = await this.loadAlias(filename, dir, env);
+    let alias = await this.loadAlias(filename, dir, env);
+    if (Array.isArray(alias)) {
+      if (alias[0] === 'global') {
+        return {
+          filePath: `${alias[1]}.js`,
+          code: `module.exports=${alias[1]};`,
+        };
+      }
+      filename = alias[1];
+    } else if (typeof alias === 'string') {
+      filename = alias;
+    }
 
     // Return just the file path if this is a file, not in node_modules
     if (path.isAbsolute(filename)) {
@@ -747,7 +765,16 @@ export default class NodeResolver {
       let f = file + ext;
 
       if (expandAliases) {
-        let alias = await this.resolveAliases(file + ext, env, pkg);
+        let alias = await this.resolveAliases(f, env, pkg);
+        if (Array.isArray(alias)) {
+          if (alias[0] === 'global') {
+            alias = f;
+          } else {
+            alias = alias[1];
+          }
+        } else if (alias === false) {
+          alias = f;
+        }
         if (alias !== f) {
           res = res.concat(
             await this.expandFile(alias, extensions, env, pkg, false),
@@ -766,12 +793,13 @@ export default class NodeResolver {
     env: Environment,
     pkg: InternalPackageJSON | null,
   ) {
+    let localAliases = await this.resolvePackageAliases(filename, env, pkg);
+    if (Array.isArray(localAliases) || typeof localAliases === 'boolean') {
+      return localAliases;
+    }
+
     // First resolve local package aliases, then project global ones.
-    return this.resolvePackageAliases(
-      await this.resolvePackageAliases(filename, env, pkg),
-      env,
-      this.rootPackage,
-    );
+    return this.resolvePackageAliases(filename, env, this.rootPackage);
   }
 
   async resolvePackageAliases(
@@ -783,14 +811,14 @@ export default class NodeResolver {
       return filename;
     }
 
-    // Resolve aliases in the package.source, package.alias, and package.browser fields.
-    return (
-      (await this.getAlias(filename, pkg.pkgdir, pkg.source)) ||
-      (await this.getAlias(filename, pkg.pkgdir, pkg.alias)) ||
-      (env.isBrowser() &&
-        (await this.getAlias(filename, pkg.pkgdir, pkg.browser))) ||
-      filename
-    );
+    let pkgKeys = ['source', 'alias'];
+    if (env.isBrowser()) pkgKeys.push('browser');
+
+    for (let pkgKey of pkgKeys) {
+      let alias = await this.getAlias(filename, pkg.pkgdir, pkg[pkgKey]);
+      if (alias != null) return alias;
+    }
+    return filename;
   }
 
   async getAlias(filename: FilePath, dir: FilePath, aliases: ?Aliases) {
@@ -803,18 +831,14 @@ export default class NodeResolver {
     // If filename is an absolute path, get one relative to the package.json directory.
     if (path.isAbsolute(filename)) {
       filename = relativePath(dir, filename);
-      alias = await this.lookupAlias(aliases, filename, dir);
+      alias = await this.lookupAlias(aliases, filename);
     } else {
       // It is a node_module. First try the entire filename as a key.
-      alias = await this.lookupAlias(
-        aliases,
-        normalizeSeparators(filename),
-        dir,
-      );
+      alias = await this.lookupAlias(aliases, normalizeSeparators(filename));
       if (alias == null) {
         // If it didn't match, try only the module name.
         let [moduleName, subPath] = this.getModuleParts(filename);
-        alias = await this.lookupAlias(aliases, moduleName, dir);
+        alias = await this.lookupAlias(aliases, moduleName);
         if (typeof alias === 'string' && subPath) {
           // Append the filename back onto the aliased module.
           alias = path.join(alias, subPath);
@@ -824,13 +848,34 @@ export default class NodeResolver {
 
     // If the alias is set to `false`, return an empty file.
     if (alias === false) {
-      return EMPTY_SHIM;
+      return ['file', EMPTY_SHIM];
     }
 
-    return alias;
+    if (alias instanceof Object) {
+      if (alias.global) {
+        if (typeof alias.global !== 'string' || alias.global.length === 0) {
+          throw new ThrowableDiagnostic({
+            diagnostic: {
+              message: `The global alias for ${filename} is invalid.`,
+              hints: [`Only nonzero-length strings are valid global aliases.`],
+            },
+          });
+        }
+        return ['global', alias.global];
+      } else if (alias.fileName) {
+        alias = alias.fileName;
+      }
+    }
+
+    if (typeof alias === 'string') {
+      // Assume file
+      return ['file', await this.resolveFilename(alias, dir)];
+    }
+
+    return null;
   }
 
-  lookupAlias(aliases: Aliases, filename: FilePath, dir: FilePath) {
+  lookupAlias(aliases: Aliases, filename: FilePath) {
     if (typeof aliases !== 'object') {
       return null;
     }
@@ -850,14 +895,7 @@ export default class NodeResolver {
         }
       }
     }
-
-    if (typeof alias === 'string') {
-      return this.resolveFilename(alias, dir);
-    } else if (alias === false) {
-      return false;
-    }
-
-    return null;
+    return alias;
   }
 
   async findPackage(dir: string) {


Test patch:
diff --git a/packages/core/integration-tests/test/globals.js b/packages/core/integration-tests/test/globals.js
new file mode 100644
index 000000000..a7b849afa
--- /dev/null
+++ b/packages/core/integration-tests/test/globals.js
@@ -0,0 +1,22 @@
+import assert from 'assert';
+import path from 'path';
+import {bundle, run} from '@parcel/test-utils';
+
+describe('global alias', function() {
+  it('should support global alias syntax', async function() {
+    let b = await bundle(
+      path.join(__dirname, '/integration/global-alias/index.js'),
+    );
+
+    assert.equal(
+      await run(b, {
+        React: {
+          createElement: function() {
+            return 'ok';
+          },
+        },
+      }),
+      'ok',
+    );
+  });
+});
diff --git a/packages/core/integration-tests/test/integration/global-alias/index.js b/packages/core/integration-tests/test/integration/global-alias/index.js
new file mode 100644
index 000000000..832e89109
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/global-alias/index.js
@@ -0,0 +1,3 @@
+import React from 'react';
+
+module.exports = React.createElement('div');
\ No newline at end of file
diff --git a/packages/core/integration-tests/test/integration/global-alias/package.json b/packages/core/integration-tests/test/integration/global-alias/package.json
new file mode 100644
index 000000000..8a5c0216f
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/global-alias/package.json
@@ -0,0 +1,6 @@
+{
+  "main": "dist/index.js",
+  "alias": {
+    "react": { "global": "React" }
+  }
+}
\ No newline at end of file