Repository: parcel-bundler/parcel
Pull request #5107 (2020-09-06 19:28:14)

Parcel config diagnostics
Adds diagnostics for errors in `.parcelrc` files for the following scenarios:

* When an `"extends"` field contains one or more invalid references. Includes fuzzy match suggestions if possible for both node_modules and file paths. If there are multiple extended configs that don't exist, multiple code frames will be emitted together.
* When a parcel plugin referenced in a config cannot be resolved. Shows a code frame for the parcelrc location that referenced it, and includes fuzzy match suggestions if possible. This required passing through key paths in the config around so we can track where a plugin was referenced.
* When a Parcel plugin is not compatible with the current version of parcel. Shows a code frame for the package.json where the `engines.parcel` field is defined.

In order to implement the fuzzy matching, I've moved the `findAlternativeNodeModules` and `findAlternativeFiles` functions out of the resolver plugin and into `@parcel/utils` so it can be used in other places.

<img width="1207" alt="image" src="https://user-images.githubusercontent.com/19409/92333597-adedc900-f03b-11ea-8ee6-3e292d7173a0.png">


height[bot]: > Link [Height](https://parcel.height.app) tasks by mentioning a task ID in the pull request title or description, commit messages, or comments.
>
> 💡Tip: You can also use "Close T-X" to automatically close a task when the pull request is merged.
<!-- Sent by Height -->
parcel-benchmark: ## Benchmark Results
<details><summary>Kitchen Sink ✅</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | 7.60s | +69.00ms |
| Cached | 2.57s | -43.00ms |

#### Cold Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/legacy/parcel.d5807e82.webp | 102.94kb | +0.00b | 309.00ms | +49.00ms ⚠️ |
| dist/modern/parcel.d5807e82.webp | 102.94kb | +0.00b | 308.00ms | +48.00ms ⚠️ |
| dist/legacy/index.html | 701.00b | +0.00b | 831.00ms | +76.00ms ⚠️ |
| dist/modern/index.html | 701.00b | +0.00b | 828.00ms | +79.00ms ⚠️ |
| dist/legacy/index.b3acc8eb.css | 77.00b | +0.00b | 1.71s | -266.00ms 🚀 |
| dist/modern/index.7bc22a8b.css | 77.00b | +0.00b | 1.75s | +159.00ms ⚠️ |

#### Cached Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/legacy/index.45cccf70.js | 1.10kb | +0.00b | 598.00ms | -43.00ms 🚀 |
| dist/modern/index.1964d1f6.js | 1.10kb | +0.00b | 597.00ms | -43.00ms 🚀 |
| dist/legacy/index.html | 701.00b | +0.00b | 600.00ms | -50.00ms 🚀 |
| dist/modern/index.html | 701.00b | +0.00b | 597.00ms | -42.00ms 🚀 |
| dist/legacy/index.b3acc8eb.css | 77.00b | +0.00b | 598.00ms | -42.00ms 🚀 |
| dist/modern/index.7bc22a8b.css | 77.00b | +0.00b | 597.00ms | -43.00ms 🚀 |

</p></details>

<details><summary>React HackerNews ✅</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | 35.38s | +1.39s |
| Cached | 2.76s | -54.00ms |

#### Cold Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/index.js | 478.68kb | +0.00b | 12.91s | -708.00ms 🚀 |
| dist/PermalinkedComment.9c5ad065.js | 4.22kb | +0.00b | 12.84s | -722.00ms 🚀 |
| dist/logo.24c8bf9e.png | 274.00b | +0.00b | 165.00ms | -17.00ms 🚀 |

#### Cached Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/index.js | 478.68kb | +0.00b | 288.00ms | -94.00ms 🚀 |
| dist/PermalinkedComment.9c5ad065.js | 4.22kb | +0.00b | 285.00ms | -82.00ms 🚀 |
| dist/UserProfile.c1927888.js | 1.62kb | +0.00b | 286.00ms | -81.00ms 🚀 |
| dist/NotFound.f8a0616d.js | 387.00b | +0.00b | 286.00ms | -80.00ms 🚀 |
| dist/logo.24c8bf9e.png | 274.00b | +0.00b | 201.00ms | -102.00ms 🚀 |

</p></details>

<details><summary>AtlasKit Editor ✅</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | 3.37m | +630.00ms |
| Cached | 6.11s | +115.00ms |

#### Cold Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/index.474ded24.js | 2.72mb | -4.00b 🚀 | 1.75m | -1.29s |
| dist/popup.6c453e3c.js | 169.96kb | +0.00b | 1.03m | +25.42s ⚠️ |
| dist/card.0313bac8.js | 52.78kb | +0.00b | 1.75m | +40.37s ⚠️ |
| dist/smartMediaEditor.9623570c.js | 16.87kb | +0.00b | 1.07m | -42.37s 🚀 |
| dist/workerHasher.1e706e77.js | 11.75kb | +0.00b | 58.87s | +46.69s ⚠️ |
| dist/EmojiTypeAheadComponent.1990d771.js | 8.67kb | +0.00b | 58.87s | +46.75s ⚠️ |
| dist/index.ac562c80.css | 3.46kb | +0.00b | 1.32m | +11.75s ⚠️ |
| dist/Modal.0304b9ed.js | 3.08kb | +0.00b | 6.10s | +1.00s ⚠️ |
| dist/EmojiUploadComponent.d54a1fc6.js | 2.18kb | +0.00b | 58.87s | +46.75s ⚠️ |
| dist/ResourcedEmojiComponent.abe8596c.js | 2.00kb | +0.00b | 6.10s | +1.02s ⚠️ |
| dist/feedback.d5590b34.js | 1.64kb | +0.00b | 7.87s | -52.99s 🚀 |
| dist/workerHasher.6d5b22d7.js | 1.54kb | +0.00b | 58.87s | +46.74s ⚠️ |
| dist/heading6.fdbbdf4d.js | 1.30kb | +0.00b | 7.87s | -52.99s 🚀 |
| dist/heading5.e195aab1.js | 1.17kb | +0.00b | 7.87s | -52.99s 🚀 |
| dist/expand.450f3aea.js | 1.16kb | +0.00b | 7.87s | -3.36s 🚀 |
| dist/16.3920766f.js | 1.10kb | +0.00b | 6.10s | +1.00s ⚠️ |
| dist/component.6600f83e.js | 1.07kb | +0.00b | 6.10s | +1.02s ⚠️ |
| dist/heading4.ec980e93.js | 1.06kb | +0.00b | 7.87s | -52.99s 🚀 |

#### Cached Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/index.474ded24.js | 2.72mb | +3.00b ⚠️ | 582.00ms | -57.00ms 🚀 |
| dist/pdfRenderer.15a6a023.js | 1.11mb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/editorView.e06544fb.js | 587.12kb | +0.00b | 578.00ms | -58.00ms 🚀 |
| dist/popup.6c453e3c.js | 169.96kb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/Toolbar.83ac0b6e.js | 92.34kb | +0.00b | 578.00ms | -58.00ms 🚀 |
| dist/EmojiPickerComponent.ae601247.js | 75.29kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/media-viewer.bcba38b3.js | 71.96kb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/card.645a5c17.js | 55.01kb | +0.00b | 580.00ms | -56.00ms 🚀 |
| dist/card.0313bac8.js | 52.78kb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/Modal.19ffed32.js | 41.27kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/component.d7c29980.js | 30.70kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/component.54080a26.js | 22.50kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/DatePicker.0df05dac.js | 21.21kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/EmojiPickerComponent.7dc0ebd0.js | 18.23kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/dropzone.7c704bdb.js | 17.27kb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/smartMediaEditor.9623570c.js | 16.87kb | +0.00b | 578.00ms | -58.00ms 🚀 |
| dist/js.5733a017.js | 16.40kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/ui.fee77548.js | 13.97kb | +0.00b | 580.00ms | -56.00ms 🚀 |
| dist/workerHasher.1e706e77.js | 11.75kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/EmojiTypeAheadComponent.1990d771.js | 8.67kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/component.77c0f682.js | 6.05kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/card.383bd9bd.js | 5.61kb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/media-viewer.59c17dad.js | 3.92kb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/EmojiPickerComponent.627aca6b.js | 3.58kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/index.ac562c80.css | 3.46kb | +0.00b | 570.00ms | -65.00ms 🚀 |
| dist/png-chunks-extract.a844fdb6.js | 3.41kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/dropzone.3c67f0a6.js | 3.23kb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/Modal.0304b9ed.js | 3.08kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/clipboard.da112b6e.js | 2.75kb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/16.290b4a43.js | 2.38kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/EmojiUploadComponent.d54a1fc6.js | 2.18kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/card.eea6ad8c.js | 2.03kb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/ResourcedEmojiComponent.abe8596c.js | 2.00kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/date.231500d2.js | 1.73kb | +0.00b | 581.00ms | -56.00ms 🚀 |
| dist/16.d16f14ed.js | 1.73kb | +0.00b | 582.00ms | -56.00ms 🚀 |
| dist/images.80c92f5e.js | 1.67kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/16.1b6ca79f.js | 1.66kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/feedback.d5590b34.js | 1.64kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/browser.75813db0.js | 1.61kb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/workerHasher.6d5b22d7.js | 1.54kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/workerHasher.fa198233.js | 1.54kb | +0.00b | 578.00ms | -58.00ms 🚀 |
| dist/list-number.c35f9589.js | 1.46kb | +0.00b | 581.00ms | -56.00ms 🚀 |
| dist/status.8ab4226b.js | 1.46kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/16.9ccb5b28.js | 1.38kb | +0.00b | 582.00ms | -56.00ms 🚀 |
| dist/code.3dd71280.js | 1.38kb | +0.00b | 581.00ms | -56.00ms 🚀 |
| dist/16.19c93285.js | 1.33kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/16.c7751e34.js | 1.33kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/16.51049e2c.js | 1.32kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/16.18344f47.js | 1.31kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/link.2f4427e6.js | 1.30kb | +0.00b | 581.00ms | -56.00ms 🚀 |
| dist/heading6.fdbbdf4d.js | 1.30kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/heading3.2113b7a1.js | 1.29kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/16.2c4c1846.js | 1.27kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/emoji.93ae61d3.js | 1.23kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/16.d17ab186.js | 1.23kb | +0.00b | 582.00ms | -56.00ms 🚀 |
| dist/16.436f097f.js | 1.21kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/16.b4da0acb.js | 1.19kb | +0.00b | 582.00ms | -56.00ms 🚀 |
| dist/16.02c68c4f.js | 1.18kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/heading5.e195aab1.js | 1.17kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/expand.450f3aea.js | 1.16kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/media-card-analytics-error-boundary.2323af57.js | 1.15kb | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/16.b46ef690.js | 1.14kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/16.4e3b0235.js | 1.14kb | +0.00b | 582.00ms | -56.00ms 🚀 |
| dist/16.9ba230ea.js | 1.13kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/16.a37df53b.js | 1.13kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/16.9adb4ec7.js | 1.13kb | +0.00b | 582.00ms | -56.00ms 🚀 |
| dist/16.eafe1a11.js | 1.13kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/heading2.a2a1be3f.js | 1.11kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/16.3920766f.js | 1.10kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/component.6600f83e.js | 1.07kb | +0.00b | 581.00ms | -58.00ms 🚀 |
| dist/mention.4d64e059.js | 1.07kb | +0.00b | 581.00ms | -56.00ms 🚀 |
| dist/heading4.ec980e93.js | 1.06kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/16.afcb09b6.js | 1.05kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/layout.52ba15f7.js | 1.05kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/divider.16defde6.js | 1.03kb | +0.00b | 581.00ms | -56.00ms 🚀 |
| dist/quote.aa25b83c.js | 1.02kb | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/action.9aa72b39.js | 1.00kb | +0.00b | 581.00ms | -57.00ms 🚀 |
| dist/decision.20244046.js | 1010.00b | +0.00b | 581.00ms | -56.00ms 🚀 |
| dist/panel-warning.97a0e3b3.js | 1008.00b | +0.00b | 581.00ms | -55.00ms 🚀 |
| dist/list.c9147a62.js | 978.00b | +0.00b | 581.00ms | -56.00ms 🚀 |
| dist/heading1.7d899ba5.js | 976.00b | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/panel-error.8d11e8b7.js | 906.00b | +0.00b | 581.00ms | -55.00ms 🚀 |
| dist/panel.a28c56ee.js | 897.00b | +0.00b | 581.00ms | -56.00ms 🚀 |
| dist/table.5a4874ee.js | 889.00b | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/panel-success.9b923e58.js | 845.00b | +0.00b | 581.00ms | -55.00ms 🚀 |
| dist/panel-note.97a4d541.js | 841.00b | +0.00b | 581.00ms | -55.00ms 🚀 |
| dist/media-picker-analytics-error-boundary.f8ddfee3.js | 839.00b | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/media-card-analytics-error-boundary.663369b9.js | 835.00b | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/media-card-analytics-error-boundary.73ba4074.js | 835.00b | +0.00b | 578.00ms | -58.00ms 🚀 |
| dist/media-viewer-analytics-error-boundary.0afa7031.js | 755.00b | +0.00b | 579.00ms | -57.00ms 🚀 |
| dist/EmojiPickerComponent.2e54b9c6.js | 535.00b | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/simpleHasher.16c8bf41.js | 528.00b | +0.00b | 580.00ms | -57.00ms 🚀 |
| dist/simpleHasher.a29b2a8e.js | 528.00b | +0.00b | 578.00ms | -58.00ms 🚀 |
| dist/index.html | 119.00b | +0.00b | 584.00ms | -57.00ms 🚀 |

</p></details>

<details><summary>Three.js x4 🚨</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | **FAILED** | -0.00ms |
| Cached | **FAILED** | -0.00ms |

#### Cold Bundles

*No bundles found, this is probably a failed build...*
#### Cached Bundles

*No bundles found, this is probably a failed build...*
</p></details>

**[Click here to view a detailed benchmark overview.](https://benchmark.parceljs.org/benchmark/275948096620331533)**

Patch:
diff --git a/packages/core/core/src/AssetGraphBuilder.js b/packages/core/core/src/AssetGraphBuilder.js
index 7174ba7e9..5e6827dbb 100644
--- a/packages/core/core/src/AssetGraphBuilder.js
+++ b/packages/core/core/src/AssetGraphBuilder.js
@@ -199,6 +199,7 @@ export default class AssetGraphBuilder extends EventEmitter {
     let config = new ParcelConfig(
       processedConfig,
       this.options.packageManager,
+      this.options.inputFS,
       this.options.autoinstall,
     );
     let trackedRequestsDesc = this.assetRequests.filter(request => {
diff --git a/packages/core/core/src/BundlerRunner.js b/packages/core/core/src/BundlerRunner.js
index d93f983c5..73e83ee42 100644
--- a/packages/core/core/src/BundlerRunner.js
+++ b/packages/core/core/src/BundlerRunner.js
@@ -192,6 +192,7 @@ export default class BundlerRunner {
       version: string,
       plugin: Namer,
       resolveFrom: FilePath,
+      keyPath: string,
     |}>,
     internalBundle: InternalBundle,
     internalBundleGraph: InternalBundleGraph,
diff --git a/packages/core/core/src/ConfigLoader.js b/packages/core/core/src/ConfigLoader.js
index 2101f95f4..3776009d0 100644
--- a/packages/core/core/src/ConfigLoader.js
+++ b/packages/core/core/src/ConfigLoader.js
@@ -90,7 +90,7 @@ export default class ConfigLoader {
     env,
     isSource,
     filePath,
-    meta: {parcelConfigPath},
+    meta: {parcelConfigPath, parcelConfigKeyPath},
   }: ConfigRequestDesc): Promise<Config> {
     let config = createConfig({
       isSource,
@@ -99,9 +99,11 @@ export default class ConfigLoader {
     });
 
     invariant(typeof parcelConfigPath === 'string');
+    invariant(typeof parcelConfigKeyPath === 'string');
     let {plugin: pluginInstance} = await this.parcelConfig.loadPlugin({
       packageName: nullthrows(plugin),
       resolveFrom: parcelConfigPath,
+      keyPath: parcelConfigKeyPath,
     });
 
     if (pluginInstance.loadConfig != null) {
diff --git a/packages/core/core/src/ParcelConfig.js b/packages/core/core/src/ParcelConfig.js
index ac6bfa8cc..6b415565c 100644
--- a/packages/core/core/src/ParcelConfig.js
+++ b/packages/core/core/src/ParcelConfig.js
@@ -16,6 +16,7 @@ import type {
   Validator,
 } from '@parcel/types';
 import type {PackageManager} from '@parcel/package-manager';
+import type {FileSystem} from '@parcel/fs';
 import type {
   ProcessedParcelConfig,
   ParcelPluginNode,
@@ -31,11 +32,21 @@ type SerializedParcelConfig = {|
   $$raw: boolean,
   config: ProcessedParcelConfig,
   packageManager: PackageManager,
+  fs: FileSystem,
   autoinstall: boolean,
 |};
 
+type LoadedPlugin<T> = {|
+  name: string,
+  version: Semver,
+  plugin: T,
+  resolveFrom: FilePath,
+  keyPath: string,
+|};
+
 export default class ParcelConfig {
   packageManager: PackageManager;
+  fs: FileSystem;
   filePath: FilePath;
   resolvers: PureParcelConfigPipeline;
   transformers: GlobMap<ExtendableParcelConfigPipeline>;
@@ -52,9 +63,11 @@ export default class ParcelConfig {
   constructor(
     config: ProcessedParcelConfig,
     packageManager: PackageManager,
+    fs: FileSystem,
     autoinstall: boolean,
   ) {
     this.packageManager = packageManager;
+    this.fs = fs;
     this.filePath = config.filePath;
     this.resolvers = config.resolvers || [];
     this.transformers = config.transformers || {};
@@ -73,6 +86,7 @@ export default class ParcelConfig {
     return new ParcelConfig(
       serialized.config,
       serialized.packageManager,
+      serialized.fs,
       serialized.autoinstall,
     );
   }
@@ -96,6 +110,7 @@ export default class ParcelConfig {
     return {
       $$raw: false,
       packageManager: this.packageManager,
+      fs: this.fs,
       config: this.getConfig(),
       autoinstall: this.autoinstall,
     };
@@ -110,9 +125,11 @@ export default class ParcelConfig {
     }
 
     plugin = loadPlugin<T>(
+      this.fs,
       this.packageManager,
       node.packageName,
       node.resolveFrom,
+      node.keyPath,
       this.autoinstall,
     );
     this.pluginCache.set(node.packageName, plugin);
@@ -121,14 +138,7 @@ export default class ParcelConfig {
 
   loadPlugins<T>(
     plugins: PureParcelConfigPipeline,
-  ): Promise<
-    Array<{|
-      name: string,
-      version: Semver,
-      plugin: T,
-      resolveFrom: FilePath,
-    |}>,
-  > {
+  ): Promise<Array<LoadedPlugin<T>>> {
     return Promise.all(
       plugins.map(async p => {
         let {plugin, version} = await this.loadPlugin<T>(p);
@@ -137,6 +147,7 @@ export default class ParcelConfig {
           plugin,
           version,
           resolveFrom: p.resolveFrom,
+          keyPath: p.keyPath,
         };
       }),
     );
@@ -154,14 +165,7 @@ export default class ParcelConfig {
     return this._getResolverNodes().map(r => r.packageName);
   }
 
-  getResolvers(): Promise<
-    Array<{|
-      name: string,
-      plugin: Resolver,
-      resolveFrom: FilePath,
-      version: Semver,
-    |}>,
-  > {
+  getResolvers(): Promise<Array<LoadedPlugin<Resolver>>> {
     return this.loadPlugins<Resolver>(this._getResolverNodes());
   }
 
@@ -179,16 +183,7 @@ export default class ParcelConfig {
     return validators.map(v => v.packageName);
   }
 
-  getValidators(
-    filePath: FilePath,
-  ): Promise<
-    Array<{|
-      name: string,
-      plugin: Validator,
-      resolveFrom: FilePath,
-      version: Semver,
-    |}>,
-  > {
+  getValidators(filePath: FilePath): Promise<Array<LoadedPlugin<Validator>>> {
     let validators = this._getValidatorNodes(filePath);
     return this.loadPlugins<Validator>(validators);
   }
@@ -237,14 +232,7 @@ export default class ParcelConfig {
     filePath: FilePath,
     pipeline?: ?string,
     allowEmpty?: boolean,
-  ): Promise<
-    Array<{|
-      name: string,
-      plugin: Transformer,
-      resolveFrom: FilePath,
-      version: Semver,
-    |}>,
-  > {
+  ): Promise<Array<LoadedPlugin<Transformer>>> {
     return this.loadPlugins<Transformer>(
       this._getTransformerNodes(filePath, pipeline, allowEmpty),
     );
@@ -266,14 +254,7 @@ export default class ParcelConfig {
     return this.loadPlugin<Bundler>(this.bundler);
   }
 
-  getNamers(): Promise<
-    Array<{|
-      name: string,
-      plugin: Namer,
-      resolveFrom: FilePath,
-      version: Semver,
-    |}>,
-  > {
+  getNamers(): Promise<Array<LoadedPlugin<Namer>>> {
     if (this.namers.length === 0) {
       throw new Error('No namer plugins specified in .parcelrc config');
     }
@@ -283,14 +264,7 @@ export default class ParcelConfig {
 
   getRuntimes(
     context: EnvironmentContext,
-  ): Promise<
-    Array<{|
-      name: string,
-      version: Semver,
-      plugin: Runtime,
-      resolveFrom: FilePath,
-    |}>,
-  > {
+  ): Promise<Array<LoadedPlugin<Runtime>>> {
     let runtimes = this.runtimes[context];
     if (!runtimes) {
       return Promise.resolve([]);
@@ -345,14 +319,7 @@ export default class ParcelConfig {
   getOptimizers(
     filePath: FilePath,
     pipeline: ?string,
-  ): Promise<
-    Array<{|
-      name: string,
-      version: Semver,
-      plugin: Optimizer,
-      resolveFrom: FilePath,
-    |}>,
-  > {
+  ): Promise<Array<LoadedPlugin<Optimizer>>> {
     let optimizers = this._getOptimizerNodes(filePath, pipeline);
     if (optimizers.length === 0) {
       return Promise.resolve([]);
@@ -361,14 +328,7 @@ export default class ParcelConfig {
     return this.loadPlugins<Optimizer>(optimizers);
   }
 
-  getReporters(): Promise<
-    Array<{|
-      name: string,
-      plugin: Reporter,
-      resolveFrom: FilePath,
-      version: Semver,
-    |}>,
-  > {
+  getReporters(): Promise<Array<LoadedPlugin<Reporter>>> {
     return this.loadPlugins<Reporter>(this.reporters);
   }
 
diff --git a/packages/core/core/src/Transformation.js b/packages/core/core/src/Transformation.js
index 0d9346d3b..79dda29c6 100644
--- a/packages/core/core/src/Transformation.js
+++ b/packages/core/core/src/Transformation.js
@@ -148,13 +148,20 @@ export default class Transformation {
     for (let {request, result} of this.configRequests) {
       if (request.plugin != null) {
         let resolveFrom = request.meta.parcelConfigPath;
-        if (typeof resolveFrom !== 'string') {
-          throw new Error('request.meta.parcelConfigPath should be a string!');
-        }
+        let keyPath = request.meta.parcelConfigKeyPath;
+        invariant(
+          typeof resolveFrom === 'string',
+          'request.meta.parcelConfigPath should be a string!',
+        );
+        invariant(
+          typeof keyPath === 'string',
+          'request.meta.parcelConfigKeyPath should be a string!',
+        );
 
         let {plugin} = await this.parcelConfig.loadPlugin({
           packageName: request.plugin,
           resolveFrom,
+          keyPath,
         });
 
         if (plugin && plugin.preSerializeConfig) {
@@ -312,6 +319,7 @@ export default class Transformation {
             transformer.plugin,
             transformer.name,
             transformer.config,
+            transformer.configKeyPath,
             this.parcelConfig,
           );
 
@@ -321,6 +329,7 @@ export default class Transformation {
                 result,
                 transformer.name,
                 this.parcelConfig.filePath,
+                transformer.configKeyPath,
               ),
             );
           }
@@ -448,11 +457,12 @@ export default class Transformation {
       request?.isURL,
     );
 
-    for (let {name, resolveFrom} of transformers) {
+    for (let {name, resolveFrom, keyPath} of transformers) {
       let thirdPartyConfig = await this.loadTransformerConfig({
         filePath,
         plugin: name,
         parcelConfigPath: resolveFrom,
+        parcelConfigKeyPath: keyPath,
         isSource,
       });
 
@@ -464,6 +474,7 @@ export default class Transformation {
       transformers: transformers.map(transformer => ({
         name: transformer.name,
         config: configs.get(transformer.name)?.result,
+        configKeyPath: transformer.keyPath,
         plugin: transformer.plugin,
       })),
       configs,
@@ -510,11 +521,13 @@ export default class Transformation {
     filePath,
     plugin,
     parcelConfigPath,
+    parcelConfigKeyPath,
     isSource,
   }: {|
     filePath: FilePath,
     plugin: PackageName,
     parcelConfigPath: FilePath,
+    parcelConfigKeyPath: string,
     isSource: boolean,
   |}): Promise<Config> {
     let configRequest = {
@@ -524,6 +537,7 @@ export default class Transformation {
       isSource,
       meta: {
         parcelConfigPath,
+        parcelConfigKeyPath,
       },
     };
 
@@ -547,6 +561,7 @@ type TransformerWithNameAndConfig = {|
   name: PackageName,
   plugin: Transformer,
   config: ?Config,
+  configKeyPath: string,
 |};
 
 async function runTransformer(
@@ -555,6 +570,7 @@ async function runTransformer(
   transformer: Transformer,
   transformerName: string,
   preloadedConfig: ?Config,
+  configKeyPath: string,
   parcelConfig: ParcelConfig,
 ): Promise<Array<TransformerResult>> {
   const logger = new PluginLogger({origin: transformerName});
@@ -658,6 +674,7 @@ async function runTransformer(
             result,
             transformerName,
             parcelConfig.filePath,
+            configKeyPath,
           ),
         ),
       );
diff --git a/packages/core/core/src/UncommittedAsset.js b/packages/core/core/src/UncommittedAsset.js
index 45785f6f6..c137fba8f 100644
--- a/packages/core/core/src/UncommittedAsset.js
+++ b/packages/core/core/src/UncommittedAsset.js
@@ -287,6 +287,7 @@ export default class UncommittedAsset {
     result: TransformerResult,
     plugin: PackageName,
     configPath: FilePath,
+    configKeyPath: string,
   ): UncommittedAsset {
     let content = result.content ?? null;
 
@@ -328,6 +329,7 @@ export default class UncommittedAsset {
           : null,
         plugin,
         configPath,
+        configKeyPath,
       }),
       options: this.options,
       content,
diff --git a/packages/core/core/src/assetUtils.js b/packages/core/core/src/assetUtils.js
index 25ff4ff44..2a80d7764 100644
--- a/packages/core/core/src/assetUtils.js
+++ b/packages/core/core/src/assetUtils.js
@@ -55,6 +55,7 @@ type AssetOptions = {|
   uniqueKey?: ?string,
   plugin?: PackageName,
   configPath?: FilePath,
+  configKeyPath?: string,
 |};
 
 export function createAsset(options: AssetOptions): Asset {
@@ -96,6 +97,7 @@ export function createAsset(options: AssetOptions): Asset {
     uniqueKey: uniqueKey,
     plugin: options.plugin,
     configPath: options.configPath,
+    configKeyPath: options.configKeyPath,
   };
 }
 
@@ -120,9 +122,11 @@ async function _generateFromAST(asset: CommittedAsset | UncommittedAsset) {
 
   let pluginName = nullthrows(asset.value.plugin);
   let {plugin} = await loadPlugin<Transformer>(
+    asset.options.inputFS,
     asset.options.packageManager,
     pluginName,
     nullthrows(asset.value.configPath),
+    nullthrows(asset.value.configKeyPath),
     asset.options.autoinstall,
   );
   if (!plugin.generate) {
diff --git a/packages/core/core/src/loadParcelPlugin.js b/packages/core/core/src/loadParcelPlugin.js
index d9c6e838d..c664380bb 100644
--- a/packages/core/core/src/loadParcelPlugin.js
+++ b/packages/core/core/src/loadParcelPlugin.js
@@ -1,25 +1,61 @@
 // @flow
 import type {FilePath, PackageName, Semver} from '@parcel/types';
 import type {PackageManager} from '@parcel/package-manager';
+import type {FileSystem} from '@parcel/fs';
 
 import semver from 'semver';
 import logger from '@parcel/logger';
 import {CONFIG} from '@parcel/plugin';
 import nullthrows from 'nullthrows';
+import ThrowableDiagnostic, {
+  generateJSONCodeHighlights,
+} from '@parcel/diagnostic';
+import {findAlternativeNodeModules, resolveConfig} from '@parcel/utils';
+import path from 'path';
 
 const PARCEL_VERSION = require('../package.json').version;
 
 export default async function loadPlugin<T>(
+  fs: FileSystem,
   packageManager: PackageManager,
   pluginName: PackageName,
   resolveFrom: FilePath,
+  keyPath: string,
   autoinstall: boolean,
 ): Promise<{|plugin: T, version: Semver|}> {
-  let {resolved, pkg} = await packageManager.resolve(
-    pluginName,
-    `${resolveFrom}/index`,
-    {autoinstall},
-  );
+  let resolved, pkg;
+  try {
+    ({resolved, pkg} = await packageManager.resolve(pluginName, resolveFrom, {
+      autoinstall,
+    }));
+  } catch (err) {
+    let configContents = await fs.readFile(resolveFrom, 'utf8');
+    let alternatives = await findAlternativeNodeModules(
+      fs,
+      pluginName,
+      path.dirname(resolveFrom),
+    );
+    throw new ThrowableDiagnostic({
+      diagnostic: {
+        message: `Cannot find parcel plugin "${pluginName}"`,
+        origin: '@parcel/core',
+        filePath: resolveFrom,
+        language: 'json5',
+        codeFrame: {
+          code: configContents,
+          codeHighlights: generateJSONCodeHighlights(configContents, [
+            {
+              key: keyPath,
+              type: 'value',
+              message: `Cannot find module "${pluginName}"${
+                alternatives[0] ? `, did you mean "${alternatives[0]}"?` : ''
+              }`,
+            },
+          ]),
+        },
+      },
+    });
+  }
 
   // Validate the engines.parcel field in the plugin's package.json
   let parcelVersionRange = pkg && pkg.engines && pkg.engines.parcel;
@@ -34,12 +70,29 @@ export default async function loadPlugin<T>(
     parcelVersionRange &&
     !semver.satisfies(PARCEL_VERSION, parcelVersionRange)
   ) {
-    throw new Error(
-      `The plugin "${pluginName}" is not compatible with the current version of Parcel. Requires "${parcelVersionRange}" but the current version is "${PARCEL_VERSION}".`,
+    let pkgFile = nullthrows(
+      await resolveConfig(fs, resolved, ['package.json']),
     );
+    let pkgContents = await fs.readFile(pkgFile, 'utf8');
+    throw new ThrowableDiagnostic({
+      diagnostic: {
+        message: `The plugin "${pluginName}" is not compatible with the current version of Parcel. Requires "${parcelVersionRange}" but the current version is "${PARCEL_VERSION}".`,
+        origin: '@parcel/core',
+        filePath: pkgFile,
+        language: 'json5',
+        codeFrame: {
+          code: pkgContents,
+          codeHighlights: generateJSONCodeHighlights(pkgContents, [
+            {
+              key: '/engines/parcel',
+            },
+          ]),
+        },
+      },
+    });
   }
 
-  let plugin = await packageManager.require(resolved, `${resolveFrom}/index`, {
+  let plugin = await packageManager.require(resolved, resolveFrom, {
     autoinstall,
   });
   plugin = plugin.default ? plugin.default : plugin;
diff --git a/packages/core/core/src/requests/ParcelConfigRequest.js b/packages/core/core/src/requests/ParcelConfigRequest.js
index 53ede1f5e..e2190f2cb 100644
--- a/packages/core/core/src/requests/ParcelConfigRequest.js
+++ b/packages/core/core/src/requests/ParcelConfigRequest.js
@@ -19,8 +19,13 @@ import {
   resolveConfig,
   resolve,
   validateSchema,
+  findAlternativeNodeModules,
+  findAlternativeFiles,
+  flatMap,
 } from '@parcel/utils';
-import ThrowableDiagnostic from '@parcel/diagnostic';
+import ThrowableDiagnostic, {
+  generateJSONCodeHighlights,
+} from '@parcel/diagnostic';
 // $FlowFixMe
 import {parse} from 'json5';
 import path from 'path';
@@ -129,7 +134,22 @@ export async function resolveParcelConfig(
     return null;
   }
 
-  return readAndProcessConfigChain(configPath, options);
+  let contents;
+  try {
+    contents = await options.inputFS.readFile(configPath, 'utf8');
+  } catch (e) {
+    throw new ThrowableDiagnostic({
+      diagnostic: {
+        message: `Could not find parcel config at ${path.relative(
+          options.projectRoot,
+          configPath,
+        )}`,
+        origin: '@parcel/core',
+      },
+    });
+  }
+
+  return parseAndProcessConfig(configPath, contents, options);
 }
 
 export function create(
@@ -139,11 +159,12 @@ export function create(
   return processConfigChain(config, config.filePath, options);
 }
 
-export async function readAndProcessConfigChain(
+// eslint-disable-next-line require-await
+export async function parseAndProcessConfig(
   configPath: FilePath,
+  contents: string,
   options: ParcelOptions,
 ): Promise<ParcelConfigChain> {
-  let contents = await options.inputFS.readFile(configPath, 'utf8');
   let config: RawParcelConfig;
   try {
     config = parse(contents);
@@ -177,16 +198,18 @@ export async function readAndProcessConfigChain(
 
 function processPipeline(
   pipeline: ?Array<PackageName>,
+  keyPath: string,
   filePath: FilePath,
   //$FlowFixMe
 ): any {
   if (pipeline) {
-    return pipeline.map(pkg => {
+    return pipeline.map((pkg, i) => {
       if (pkg === '...') return pkg;
 
       return {
         packageName: pkg,
         resolveFrom: filePath,
+        keyPath: `${keyPath}/${i}`,
       };
     });
   }
@@ -195,6 +218,7 @@ function processPipeline(
 function processMap(
   // $FlowFixMe
   map: ?ConfigMap<any, any>,
+  keyPath: string,
   filePath: FilePath,
   // $FlowFixMe
 ): ConfigMap<any, any> | typeof undefined {
@@ -207,9 +231,10 @@ function processMap(
       res[k] = {
         packageName: map[k],
         resolveFrom: filePath,
+        keyPath: `${keyPath}/${k}`,
       };
     } else {
-      res[k] = processPipeline(map[k], filePath);
+      res[k] = processPipeline(map[k], `${keyPath}/${k}`, filePath);
     }
   }
 
@@ -223,21 +248,46 @@ export function processConfig(
     extends: configFile.extends,
     filePath: configFile.filePath,
     resolveFrom: configFile.resolveFrom,
-    resolvers: processPipeline(configFile.resolvers, configFile.filePath),
-    transformers: processMap(configFile.transformers, configFile.filePath),
+    resolvers: processPipeline(
+      configFile.resolvers,
+      '/resolvers',
+      configFile.filePath,
+    ),
+    transformers: processMap(
+      configFile.transformers,
+      '/transformers',
+      configFile.filePath,
+    ),
     bundler:
       configFile.bundler != null
         ? {
             packageName: configFile.bundler,
             resolveFrom: configFile.filePath,
+            keyPath: '/bundler',
           }
         : undefined,
-    namers: processPipeline(configFile.namers, configFile.filePath),
-    runtimes: processMap(configFile.runtimes, configFile.filePath),
-    packagers: processMap(configFile.packagers, configFile.filePath),
-    optimizers: processMap(configFile.optimizers, configFile.filePath),
-    reporters: processPipeline(configFile.reporters, configFile.filePath),
-    validators: processMap(configFile.validators, configFile.filePath),
+    namers: processPipeline(configFile.namers, '/namers', configFile.filePath),
+    runtimes: processMap(configFile.runtimes, '/runtimes', configFile.filePath),
+    packagers: processMap(
+      configFile.packagers,
+      '/packagers',
+      configFile.filePath,
+    ),
+    optimizers: processMap(
+      configFile.optimizers,
+      '/optimizers',
+      configFile.filePath,
+    ),
+    reporters: processPipeline(
+      configFile.reporters,
+      '/reporters',
+      configFile.filePath,
+    ),
+    validators: processMap(
+      configFile.validators,
+      '/validators',
+      configFile.filePath,
+    ),
   };
 }
 
@@ -258,6 +308,7 @@ export async function processConfigChain(
   let config = new ParcelConfig(
     resolvedFile,
     options.packageManager,
+    options.inputFS,
     options.autoinstall,
   );
 
@@ -266,27 +317,48 @@ export async function processConfigChain(
     let exts = Array.isArray(configFile.extends)
       ? configFile.extends
       : [configFile.extends];
+    let errors = [];
     if (exts.length !== 0) {
-      let [extStart, ...otherExts] = exts;
-      let extStartResolved = await resolveExtends(extStart, filePath, options);
-      extendedFiles.push(extStartResolved);
-      let {
-        extendedFiles: extStartMoreExtendedFiles,
-        config: extStartConfig,
-      } = await readAndProcessConfigChain(extStartResolved, options);
-      extendedFiles = extendedFiles.concat(extStartMoreExtendedFiles);
-      for (let ext of otherExts) {
-        let resolved = await resolveExtends(ext, filePath, options);
-        extendedFiles.push(resolved);
-        let {
-          extendedFiles: moreExtendedFiles,
-          config: nextConfig,
-        } = await readAndProcessConfigChain(resolved, options);
-        extendedFiles = extendedFiles.concat(moreExtendedFiles);
-        extStartConfig = mergeConfigs(extStartConfig, nextConfig);
+      let extStartConfig;
+      let i = 0;
+      for (let ext of exts) {
+        try {
+          let key = Array.isArray(configFile.extends)
+            ? `/extends/${i}`
+            : '/extends';
+          let resolved = await resolveExtends(ext, filePath, key, options);
+          extendedFiles.push(resolved);
+          let {
+            extendedFiles: moreExtendedFiles,
+            config: nextConfig,
+          } = await processExtendedConfig(
+            filePath,
+            key,
+            ext,
+            resolved,
+            options,
+          );
+          extendedFiles = extendedFiles.concat(moreExtendedFiles);
+          extStartConfig = extStartConfig
+            ? mergeConfigs(extStartConfig, nextConfig)
+            : nextConfig;
+        } catch (err) {
+          errors.push(err);
+        }
+
+        i++;
       }
+
       // Merge with the inline config last
-      config = mergeConfigs(extStartConfig, resolvedFile);
+      if (extStartConfig) {
+        config = mergeConfigs(extStartConfig, resolvedFile);
+      }
+    }
+
+    if (errors.length > 0) {
+      throw new ThrowableDiagnostic({
+        diagnostic: flatMap(errors, e => e.diagnostics),
+      });
     }
   }
 
@@ -296,17 +368,92 @@ export async function processConfigChain(
 export async function resolveExtends(
   ext: string,
   configPath: FilePath,
+  extendsKey: string,
   options: ParcelOptions,
 ): Promise<FilePath> {
   if (ext.startsWith('.')) {
     return path.resolve(path.dirname(configPath), ext);
   } else {
-    let {resolved} = await resolve(options.inputFS, ext, {
-      basedir: path.dirname(configPath),
-      extensions: ['.json'],
+    try {
+      let {resolved} = await resolve(options.inputFS, ext, {
+        basedir: path.dirname(configPath),
+        extensions: ['.json'],
+      });
+      return options.inputFS.realpath(resolved);
+    } catch (err) {
+      let parentContents = await options.inputFS.readFile(configPath, 'utf8');
+      let alternatives = await findAlternativeNodeModules(
+        options.inputFS,
+        ext,
+        path.dirname(configPath),
+      );
+      throw new ThrowableDiagnostic({
+        diagnostic: {
+          message: 'Cannot find extended parcel config',
+          origin: '@parcel/core',
+          filePath: configPath,
+          language: 'json5',
+          codeFrame: {
+            code: parentContents,
+            codeHighlights: generateJSONCodeHighlights(parentContents, [
+              {
+                key: extendsKey,
+                type: 'value',
+                message: `Cannot find module "${ext}"${
+                  alternatives[0] ? `, did you mean "${alternatives[0]}"?` : ''
+                }`,
+              },
+            ]),
+          },
+        },
+      });
+    }
+  }
+}
+
+async function processExtendedConfig(
+  configPath: FilePath,
+  extendsKey: string,
+  extendsSpecifier: string,
+  resolvedExtendedConfigPath: FilePath,
+  options: ParcelOptions,
+): Promise<ParcelConfigChain> {
+  let contents;
+  try {
+    contents = await options.inputFS.readFile(
+      resolvedExtendedConfigPath,
+      'utf8',
+    );
+  } catch (e) {
+    let parentContents = await options.inputFS.readFile(configPath, 'utf8');
+    let alternatives = await findAlternativeFiles(
+      options.inputFS,
+      extendsSpecifier,
+      path.dirname(resolvedExtendedConfigPath),
+    );
+    throw new ThrowableDiagnostic({
+      diagnostic: {
+        message: 'Cannot find extended parcel config',
+        origin: '@parcel/core',
+        filePath: configPath,
+        language: 'json5',
+        codeFrame: {
+          code: parentContents,
+          codeHighlights: generateJSONCodeHighlights(parentContents, [
+            {
+              key: extendsKey,
+              type: 'value',
+              message: `"${extendsSpecifier}" does not exist${
+                alternatives[0] ? `, did you mean "./${alternatives[0]}"?` : ''
+              }`,
+            },
+          ]),
+        },
+      },
     });
-    return options.inputFS.realpath(resolved);
   }
+
+  return parseAndProcessConfig(resolvedExtendedConfigPath, contents, options);
 }
 
 export function validateConfigFile(
@@ -356,6 +503,7 @@ export function mergeConfigs(
       reporters: mergePipelines(base.reporters, ext.reporters),
     },
     base.packageManager,
+    base.fs,
     base.autoinstall,
   );
 }
diff --git a/packages/core/core/src/requests/PathRequest.js b/packages/core/core/src/requests/PathRequest.js
index a5e04d846..d967ca861 100644
--- a/packages/core/core/src/requests/PathRequest.js
+++ b/packages/core/core/src/requests/PathRequest.js
@@ -59,6 +59,7 @@ async function run({input, api, options}: RunOpts) {
     config: new ParcelConfig(
       config,
       options.packageManager,
+      options.inputFS,
       options.autoinstall,
     ),
   });
diff --git a/packages/core/core/src/requests/TargetRequest.js b/packages/core/core/src/requests/TargetRequest.js
index 74d2288a2..4b336542e 100644
--- a/packages/core/core/src/requests/TargetRequest.js
+++ b/packages/core/core/src/requests/TargetRequest.js
@@ -94,6 +94,7 @@ async function run({input, api, options}: RunOpts) {
   let parcelConfig = new ParcelConfig(
     config,
     options.packageManager,
+    options.inputFS,
     options.autoinstall,
   );
 
diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js
index 0175a9166..a7a3303ac 100644
--- a/packages/core/core/src/types.js
+++ b/packages/core/core/src/types.js
@@ -38,6 +38,7 @@ import type {PackageManager} from '@parcel/package-manager';
 export type ParcelPluginNode = {|
   packageName: PackageName,
   resolveFrom: FilePath,
+  keyPath: string,
 |};
 
 export type PureParcelConfigPipeline = $ReadOnlyArray<ParcelPluginNode>;
@@ -130,6 +131,7 @@ export type Asset = {|
   uniqueKey: ?string,
   configPath?: FilePath,
   plugin: ?PackageName,
+  configKeyPath?: string,
 |};
 
 export type ParcelOptions = {|
diff --git a/packages/core/core/src/worker.js b/packages/core/core/src/worker.js
index c1d1eae35..6898ea005 100644
--- a/packages/core/core/src/worker.js
+++ b/packages/core/core/src/worker.js
@@ -55,6 +55,7 @@ async function loadConfig(cachePath, options) {
     // $FlowFixMe
     ((processedConfig: any): ProcessedParcelConfig),
     options.packageManager,
+    options.inputFS,
     options.autoinstall,
   );
   parcelConfigCache.set(cachePath, config);
@@ -125,6 +126,7 @@ export function runPackage(
   let config = new ParcelConfig(
     processedConfig,
     options.packageManager,
+    options.inputFS,
     options.autoinstall,
   );
 
diff --git a/packages/core/utils/src/alternatives.js b/packages/core/utils/src/alternatives.js
new file mode 100644
index 000000000..910edfca5
--- /dev/null
+++ b/packages/core/utils/src/alternatives.js
@@ -0,0 +1,124 @@
+// @flow
+import path from 'path';
+import type {FileSystem} from '@parcel/fs';
+import {fuzzySearch} from './schema';
+import {relativePath} from './path';
+import {resolveConfig} from './config';
+
+export async function findAlternativeNodeModules(
+  fs: FileSystem,
+  moduleName: string,
+  dir: string,
+): Promise<Array<string>> {
+  let potentialModules: Array<string> = [];
+  let root = path.parse(dir).root;
+  let isOrganisationModule = moduleName.startsWith('@');
+
+  while (dir !== root) {
+    // Skip node_modules directories
+    if (path.basename(dir) === 'node_modules') {
+      dir = path.dirname(dir);
+    }
+
+    try {
+      let modulesDir = path.join(dir, 'node_modules');
+      let stats = await fs.stat(modulesDir);
+      if (stats.isDirectory()) {
+        let dirContent = await fs.readdir(modulesDir);
+
+        // Filter out the modules that interest us
+        let modules = dirContent.filter(i =>
+          isOrganisationModule ? i.startsWith('@') : !i.startsWith('@'),
+        );
+
+        // If it's an organisation module, loop through all the modules of that organisation
+        if (isOrganisationModule) {
+          await Promise.all(
+            modules.map(async item => {
+              let orgDirPath = path.join(modulesDir, item);
+              let orgDirContent = await fs.readdir(orgDirPath);
+
+              // Add all org packages
+              potentialModules.push(...orgDirContent.map(i => `${item}/${i}`));
+            }),
+          );
+        }
+      }
+    } catch (err) {
+      // ignore
+    }
+
+    // Move up a directory
+    dir = path.dirname(dir);
+  }
+
+  return fuzzySearch(potentialModules, moduleName).slice(0, 2);
+}
+
+async function findAllFilesUp({
+  fs,
+  dir,
+  root,
+  basedir,
+  maxlength,
+  collected,
+}: {|
+  fs: FileSystem,
+  dir: string,
+  root: string,
+  basedir: string,
+  maxlength: number,
+  collected: Array<string>,
+|}): Promise<mixed> {
+  let dirContent = await fs.readdir(dir);
+  return Promise.all(
+    dirContent.map(async item => {
+      let fullPath = path.join(dir, item);
+      let relativeFilePath = relativePath(basedir, fullPath);
+      if (relativeFilePath.length < maxlength) {
+        let stats = await fs.stat(fullPath);
+        let isDir = stats.isDirectory();
+        if (isDir || stats.isFile()) {
+          collected.push(relativeFilePath);
+        }
+
+        // If it's a directory, run over each item within said directory...
+        if (isDir) {
+          return findAllFilesUp({
+            fs,
+            dir: fullPath,
+            root,
+            basedir,
+            maxlength,
+            collected,
+          });
+        }
+      }
+    }),
+  );
+}
+
+export async function findAlternativeFiles(
+  fs: FileSystem,
+  fileSpecifier: string,
+  dir: string,
+): Promise<Array<string>> {
+  let potentialFiles: Array<string> = [];
+  // Find our root, we won't recommend files above the package root as that's bad practise
+  let pkg = await resolveConfig(fs, path.join(dir, 'index'), ['package.json']);
+  if (!pkg) {
+    return potentialFiles;
+  }
+
+  let pkgRoot = path.dirname(pkg);
+  await findAllFilesUp({
+    fs,
+    dir: pkgRoot,
+    root: pkgRoot,
+    basedir: dir,
+    maxlength: fileSpecifier.length + 10,
+    collected: potentialFiles,
+  });
+
+  return fuzzySearch(potentialFiles, fileSpecifier).slice(0, 2);
+}
diff --git a/packages/core/utils/src/index.js b/packages/core/utils/src/index.js
index ad6184c61..8975d0033 100644
--- a/packages/core/utils/src/index.js
+++ b/packages/core/utils/src/index.js
@@ -25,6 +25,7 @@ export {default as debounce} from './debounce';
 export {default as throttle} from './throttle';
 export {default as openInBrowser} from './openInBrowser';
 
+export * from './alternatives';
 export * from './blob';
 export * from './collection';
 export * from './config';
diff --git a/packages/transformers/vue/package.json b/packages/transformers/vue/package.json
index c0bf529af..efa3f3cb1 100644
--- a/packages/transformers/vue/package.json
+++ b/packages/transformers/vue/package.json
@@ -20,8 +20,10 @@
     "parcel": "^2.0.0-alpha.1.1"
   },
   "dependencies": {
+    "@parcel/diagnostic": "^2.0.0-beta.1",
     "@parcel/plugin": "2.0.0-beta.1",
     "@parcel/source-map": "2.0.0-alpha.4.13",
+    "@parcel/utils": "^2.0.0-beta.1",
     "nullthrows": "^1.1.1",
     "semver": "^5.4.1"
   },
diff --git a/packages/utils/node-resolver-core/src/NodeResolver.js b/packages/utils/node-resolver-core/src/NodeResolver.js
index 72c3d041a..9f1a4cb7a 100644
--- a/packages/utils/node-resolver-core/src/NodeResolver.js
+++ b/packages/utils/node-resolver-core/src/NodeResolver.js
@@ -11,9 +11,10 @@ import invariant from 'assert';
 import path from 'path';
 import {
   isGlob,
-  fuzzySearch,
   relativePath,
   normalizeSeparators,
+  findAlternativeNodeModules,
+  findAlternativeFiles,
 } from '@parcel/utils';
 import ThrowableDiagnostic, {
   generateJSONCodeHighlights,
@@ -168,120 +169,6 @@ export default class NodeResolver {
     return null;
   }
 
-  async findAlternativeNodeModules(
-    moduleName: string,
-    dir: string,
-  ): Promise<Array<string>> {
-    let potentialModules: Array<string> = [];
-    let root = path.parse(dir).root;
-    let isOrganisationModule = moduleName.startsWith('@');
-
-    while (dir !== root) {
-      // Skip node_modules directories
-      if (path.basename(dir) === 'node_modules') {
-        dir = path.dirname(dir);
-      }
-
-      try {
-        let modulesDir = path.join(dir, 'node_modules');
-        let stats = await this.fs.stat(modulesDir);
-        if (stats.isDirectory()) {
-          let dirContent = await this.fs.readdir(modulesDir);
-
-          // Filter out the modules that interest us
-          let modules = dirContent.filter(i =>
-            isOrganisationModule ? i.startsWith('@') : !i.startsWith('@'),
-          );
-
-          // If it's an organisation module, loop through all the modules of that organisation
-          if (isOrganisationModule) {
-            await Promise.all(
-              modules.map(async item => {
-                let orgDirPath = path.join(modulesDir, item);
-                let orgDirContent = await this.fs.readdir(orgDirPath);
-
-                // Add all org packages
-                potentialModules.push(
-                  ...orgDirContent.map(i => `${item}/${i}`),
-                );
-              }),
-            );
-          }
-        }
-      } catch (err) {
-        // ignore
-      }
-
-      // Move up a directory
-      dir = path.dirname(dir);
-    }
-
-    return fuzzySearch(potentialModules, moduleName).slice(0, 2);
-  }
-
-  async findAllFilesUp({
-    dir,
-    root,
-    basedir,
-    maxlength,
-    collected,
-  }: {|
-    dir: string,
-    root: string,
-    basedir: string,
-    maxlength: number,
-    collected: Array<string>,
-  |}): Promise<mixed> {
-    let dirContent = await this.fs.readdir(dir);
-    return Promise.all(
-      dirContent.map(async item => {
-        let fullPath = path.join(dir, item);
-        let relativeFilePath = relativePath(basedir, fullPath);
-        if (relativeFilePath.length < maxlength) {
-          let stats = await this.fs.stat(fullPath);
-          let isDir = stats.isDirectory();
-          if (isDir || stats.isFile()) {
-            collected.push(relativeFilePath);
-          }
-
-          // If it's a directory, run over each item within said directory...
-          if (isDir) {
-            return this.findAllFilesUp({
-              dir: fullPath,
-              root,
-              basedir,
-              maxlength,
-              collected,
-            });
-          }
-        }
-      }),
-    );
-  }
-
-  async findAlternativeFiles(
-    fileSpecifier: string,
-    dir: string,
-  ): Promise<Array<string>> {
-    let potentialFiles: Array<string> = [];
-    // Find our root, we won't recommend files above the package root as that's bad practise
-    let pkg = await this.findPackage(dir);
-    if (!pkg) {
-      return potentialFiles;
-    }
-
-    let pkgRoot = pkg.pkgdir;
-    await this.findAllFilesUp({
-      dir: pkgRoot,
-      root: pkgRoot,
-      basedir: dir,
-      maxlength: fileSpecifier.length + 10,
-      collected: potentialFiles,
-    });
-
-    return fuzzySearch(potentialFiles, fileSpecifier).slice(0, 2);
-  }
-
   async resolveModule({
     filename,
     parent,
@@ -375,7 +262,8 @@ export default class NodeResolver {
         subPath,
       };
 
-      let alternativeModules = await this.findAlternativeNodeModules(
+      let alternativeModules = await findAlternativeNodeModules(
+        this.fs,
         resolved.moduleName,
         dir,
       );
@@ -492,7 +380,8 @@ export default class NodeResolver {
     if (!resolvedFile) {
       // If we can't load the file do a fuzzySearch for potential hints
       let relativeFileSpecifier = relativePath(parentdir, filename);
-      let potentialFiles = await this.findAlternativeFiles(
+      let potentialFiles = await findAlternativeFiles(
+        this.fs,
         relativeFileSpecifier,
         parentdir,
       );
@@ -651,7 +540,8 @@ export default class NodeResolver {
         }
 
         let fileSpecifier = relativePath(dir, failedEntry.filename);
-        let alternatives = await this.findAlternativeFiles(
+        let alternatives = await findAlternativeFiles(
+          this.fs,
           fileSpecifier,
           pkg.pkgdir,
         );
@@ -669,8 +559,9 @@ export default class NodeResolver {
                 {
                   key: `/${failedEntry.field}`,
                   type: 'value',
-                  message: `'${fileSpecifier}' does not exist${alternative &&
-                    `, did you mean '${alternative}'?`}'`,
+                  message: `'${fileSpecifier}' does not exist${
+                    alternative ? `, did you mean '${alternative}'?` : ''
+                  }'`,
                 },
               ]),
             },


Test patch:
diff --git a/packages/core/core/test/ParcelConfig.test.js b/packages/core/core/test/ParcelConfig.test.js
index e0e730ed4..e752de7b7 100644
--- a/packages/core/core/test/ParcelConfig.test.js
+++ b/packages/core/core/test/ParcelConfig.test.js
@@ -6,6 +6,8 @@ import sinon from 'sinon';
 import logger from '@parcel/logger';
 import {inputFS} from '@parcel/test-utils';
 import {NodePackageManager} from '@parcel/package-manager';
+import {parseAndProcessConfig} from '../src/requests/ParcelConfigRequest';
+import {DEFAULT_OPTIONS} from './test-utils';
 
 const packageManager = new NodePackageManager(inputFS);
 
@@ -19,14 +21,17 @@ describe('ParcelConfig', () => {
           '*.css': {
             packageName: 'parcel-packager-css',
             resolveFrom: '.parcelrc',
+            keyPath: '/packagers/*.css',
           },
           '*.js': {
             packageName: 'parcel-packager-js',
             resolveFrom: '.parcelrc',
+            keyPath: '/packagers/*.js',
           },
         },
       },
       packageManager,
+      inputFS,
       false,
     );
 
@@ -40,6 +45,7 @@ describe('ParcelConfig', () => {
       assert.deepEqual(result, {
         packageName: 'parcel-packager-js',
         resolveFrom: '.parcelrc',
+        keyPath: '/packagers/*.js',
       });
     });
   });
@@ -54,6 +60,7 @@ describe('ParcelConfig', () => {
             {
               packageName: 'parcel-transform-jsx',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.jsx/0',
             },
             '...',
           ],
@@ -61,11 +68,13 @@ describe('ParcelConfig', () => {
             {
               packageName: 'parcel-transform-js',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.{js,jsx}/0',
             },
           ],
         },
       },
       packageManager,
+      inputFS,
       false,
     );
 
@@ -86,6 +95,7 @@ describe('ParcelConfig', () => {
         {
           packageName: 'parcel-transform-js',
           resolveFrom: '.parcelrc',
+          keyPath: '/transformers/*.{js,jsx}/0',
         },
       ]);
     });
@@ -99,10 +109,12 @@ describe('ParcelConfig', () => {
         {
           packageName: 'parcel-transform-jsx',
           resolveFrom: '.parcelrc',
+          keyPath: '/transformers/*.jsx/0',
         },
         {
           packageName: 'parcel-transform-js',
           resolveFrom: '.parcelrc',
+          keyPath: '/transformers/*.{js,jsx}/0',
         },
       ]);
     });
@@ -125,11 +137,13 @@ describe('ParcelConfig', () => {
               {
                 packageName: 'parcel-transformer-no-engines',
                 resolveFrom: configFilePath,
+                keyPath: '/transformers/*.js/0',
               },
             ],
           },
         },
         packageManager,
+        inputFS,
         false,
       );
 
@@ -137,6 +151,7 @@ describe('ParcelConfig', () => {
       let {plugin} = await config.loadPlugin({
         packageName: 'parcel-transformer-no-engines',
         resolveFrom: configFilePath,
+        keyPath: '/transformers/*.js/0',
       });
       assert(plugin);
       assert.equal(typeof plugin.transform, 'function');
@@ -163,32 +178,97 @@ describe('ParcelConfig', () => {
           transformers: {
             '*.js': [
               {
-                packageName: 'parcel-transformer-bad-engines',
+                packageName: 'parcel-transformer-not-found',
                 resolveFrom: configFilePath,
+                keyPath: '/transformers/*.js/0',
               },
             ],
           },
         },
         packageManager,
+        inputFS,
         false,
       );
 
-      let errored = false;
-      try {
-        await config.loadPlugin({
-          packageName: 'parcel-transformer-bad-engines',
-          resolveFrom: configFilePath,
-        });
-      } catch (err) {
-        errored = true;
-        let parcelVersion = require('../package.json').version;
-        assert.equal(
-          err.message,
-          `The plugin "parcel-transformer-bad-engines" is not compatible with the current version of Parcel. Requires "5.x" but the current version is "${parcelVersion}".`,
-        );
-      }
+      let parcelVersion = require('../package.json').version;
+      let pkgJSON = path.join(
+        __dirname,
+        'fixtures',
+        'plugins',
+        'node_modules',
+        'parcel-transformer-bad-engines',
+        'package.json',
+      );
+      let code = inputFS.readFileSync(pkgJSON, 'utf8');
 
-      assert(errored, 'did not error');
+      // $FlowFixMe
+      await assert.rejects(
+        () =>
+          config.loadPlugin({
+            packageName: 'parcel-transformer-bad-engines',
+            resolveFrom: configFilePath,
+            keyPath: '/transformers/*.js/0',
+          }),
+        {
+          name: 'Error',
+          diagnostics: [
+            {
+              message: `The plugin "parcel-transformer-bad-engines" is not compatible with the current version of Parcel. Requires "5.x" but the current version is "${parcelVersion}".`,
+              origin: '@parcel/core',
+              filePath: pkgJSON,
+              language: 'json5',
+              codeFrame: {
+                code,
+                codeHighlights: [
+                  {
+                    start: {line: 5, column: 5},
+                    end: {line: 5, column: 19},
+                    message: undefined,
+                  },
+                ],
+              },
+            },
+          ],
+        },
+      );
+    });
+
+    it('should error with a codeframe if a plugin is not resolved', async () => {
+      let configFilePath = path.join(
+        __dirname,
+        'fixtures',
+        'config-plugin-not-found',
+        '.parcelrc',
+      );
+      let code = await DEFAULT_OPTIONS.inputFS.readFile(configFilePath, 'utf8');
+      let {config} = await parseAndProcessConfig(
+        configFilePath,
+        code,
+        DEFAULT_OPTIONS,
+      );
+
+      // $FlowFixMe
+      await assert.rejects(() => config.getTransformers('test.js'), {
+        name: 'Error',
+        diagnostics: [
+          {
+            message: 'Cannot find parcel plugin "@parcel/transformer-jj"',
+            origin: '@parcel/core',
+            filePath: configFilePath,
+            language: 'json5',
+            codeFrame: {
+              code,
+              codeHighlights: [
+                {
+                  start: {line: 4, column: 14},
+                  end: {line: 4, column: 37},
+                  message: `Cannot find module "@parcel/transformer-jj", did you mean "@parcel/transformer-js"?`,
+                },
+              ],
+            },
+          },
+        ],
+      });
     });
   });
 });
diff --git a/packages/core/core/test/ParcelConfigRequest.test.js b/packages/core/core/test/ParcelConfigRequest.test.js
index b5532f626..7d86d6afb 100644
--- a/packages/core/core/test/ParcelConfigRequest.test.js
+++ b/packages/core/core/test/ParcelConfigRequest.test.js
@@ -8,7 +8,7 @@ import {
   mergeMaps,
   mergeConfigs,
   resolveExtends,
-  readAndProcessConfigChain,
+  parseAndProcessConfig,
   resolveParcelConfig,
   processConfig,
 } from '../src/requests/ParcelConfigRequest';
@@ -292,6 +292,7 @@ describe('loadParcelConfig', () => {
             {
               packageName: 'parcel-transform-foo',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.js/0',
             },
           ],
           null,
@@ -300,6 +301,7 @@ describe('loadParcelConfig', () => {
           {
             packageName: 'parcel-transform-foo',
             resolveFrom: '.parcelrc',
+            keyPath: '/transformers/*.js/0',
           },
         ],
       );
@@ -311,12 +313,14 @@ describe('loadParcelConfig', () => {
           {
             packageName: 'parcel-transform-bar',
             resolveFrom: '.parcelrc',
+            keyPath: '/transformers/*.js/0',
           },
         ]),
         [
           {
             packageName: 'parcel-transform-bar',
             resolveFrom: '.parcelrc',
+            keyPath: '/transformers/*.js/0',
           },
         ],
       );
@@ -329,12 +333,14 @@ describe('loadParcelConfig', () => {
             {
               packageName: 'parcel-transform-foo',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.js/0',
             },
           ],
           [
             {
               packageName: 'parcel-transform-bar',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.js/0',
             },
           ],
         ),
@@ -342,6 +348,7 @@ describe('loadParcelConfig', () => {
           {
             packageName: 'parcel-transform-bar',
             resolveFrom: '.parcelrc',
+            keyPath: '/transformers/*.js/0',
           },
         ],
       );
@@ -354,17 +361,20 @@ describe('loadParcelConfig', () => {
             {
               packageName: 'parcel-transform-foo',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.js/0',
             },
           ],
           [
             {
               packageName: 'parcel-transform-bar',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.js/0',
             },
             '...',
             {
               packageName: 'parcel-transform-baz',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.js/2',
             },
           ],
         ),
@@ -372,14 +382,17 @@ describe('loadParcelConfig', () => {
           {
             packageName: 'parcel-transform-bar',
             resolveFrom: '.parcelrc',
+            keyPath: '/transformers/*.js/0',
           },
           {
             packageName: 'parcel-transform-foo',
             resolveFrom: '.parcelrc',
+            keyPath: '/transformers/*.js/0',
           },
           {
             packageName: 'parcel-transform-baz',
             resolveFrom: '.parcelrc',
+            keyPath: '/transformers/*.js/2',
           },
         ],
       );
@@ -392,17 +405,20 @@ describe('loadParcelConfig', () => {
             {
               packageName: 'parcel-transform-foo',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.js/0',
             },
           ],
           [
             {
               packageName: 'parcel-transform-bar',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.js/0',
             },
             '...',
             {
               packageName: 'parcel-transform-baz',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.js/2',
             },
             '...',
           ],
@@ -473,6 +489,7 @@ describe('loadParcelConfig', () => {
             {
               packageName: 'parcel-resolver-base',
               resolveFrom: '.parcelrc',
+              keyPath: '/resolvers/0',
             },
           ],
           transformers: {
@@ -480,21 +497,25 @@ describe('loadParcelConfig', () => {
               {
                 packageName: 'parcel-transform-base',
                 resolveFrom: '.parcelrc',
+                keyPath: '/transformers/*.js/0',
               },
             ],
             '*.css': [
               {
                 packageName: 'parcel-transform-css',
                 resolveFrom: '.parcelrc',
+                keyPath: '/transformers/*.css/0',
               },
             ],
           },
           bundler: {
             packageName: 'parcel-bundler-base',
             resolveFrom: '.parcelrc',
+            keyPath: '/bundler',
           },
         },
         DEFAULT_OPTIONS.packageManager,
+        DEFAULT_OPTIONS.inputFS,
         false,
       );
 
@@ -504,6 +525,7 @@ describe('loadParcelConfig', () => {
           {
             packageName: 'parcel-resolver-ext',
             resolveFrom: '.parcelrc',
+            keyPath: '/resolvers/0',
           },
           '...',
         ],
@@ -512,6 +534,7 @@ describe('loadParcelConfig', () => {
             {
               packageName: 'parcel-transform-ext',
               resolveFrom: '.parcelrc',
+              keyPath: '/transformers/*.js/0',
             },
             '...',
           ],
@@ -525,10 +548,12 @@ describe('loadParcelConfig', () => {
             {
               packageName: 'parcel-resolver-ext',
               resolveFrom: '.parcelrc',
+              keyPath: '/resolvers/0',
             },
             {
               packageName: 'parcel-resolver-base',
               resolveFrom: '.parcelrc',
+              keyPath: '/resolvers/0',
             },
           ],
           transformers: {
@@ -536,22 +561,26 @@ describe('loadParcelConfig', () => {
               {
                 packageName: 'parcel-transform-ext',
                 resolveFrom: '.parcelrc',
+                keyPath: '/transformers/*.js/0',
               },
               {
                 packageName: 'parcel-transform-base',
                 resolveFrom: '.parcelrc',
+                keyPath: '/transformers/*.js/0',
               },
             ],
             '*.css': [
               {
                 packageName: 'parcel-transform-css',
                 resolveFrom: '.parcelrc',
+                keyPath: '/transformers/*.css/0',
               },
             ],
           },
           bundler: {
             packageName: 'parcel-bundler-base',
             resolveFrom: '.parcelrc',
+            keyPath: '/bundler',
           },
           runtimes: {},
           namers: [],
@@ -560,6 +589,7 @@ describe('loadParcelConfig', () => {
           reporters: [],
         },
         DEFAULT_OPTIONS.packageManager,
+        DEFAULT_OPTIONS.inputFS,
         false,
       );
 
@@ -573,6 +603,7 @@ describe('loadParcelConfig', () => {
       let resolved = await resolveExtends(
         '../.parcelrc',
         path.join(__dirname, 'fixtures', 'config', 'subfolder', '.parcelrc'),
+        '/extends',
         DEFAULT_OPTIONS,
       );
       assert.equal(
@@ -585,13 +616,14 @@ describe('loadParcelConfig', () => {
       let resolved = await resolveExtends(
         '@parcel/config-default',
         path.join(__dirname, 'fixtures', 'config', 'subfolder', '.parcelrc'),
+        '/extends',
         DEFAULT_OPTIONS,
       );
       assert.equal(resolved, require.resolve('@parcel/config-default'));
     });
   });
 
-  describe('readAndProcessConfigChain', () => {
+  describe('parseAndProcessConfig', () => {
     it('should load and merge configs', async () => {
       let defaultConfigPath = require.resolve('@parcel/config-default');
       let defaultConfig = processConfig({
@@ -611,8 +643,9 @@ describe('loadParcelConfig', () => {
         'subfolder',
         '.parcelrc',
       );
-      let {config} = await readAndProcessConfigChain(
+      let {config} = await parseAndProcessConfig(
         subConfigFilePath,
+        DEFAULT_OPTIONS.inputFS.readFileSync(subConfigFilePath, 'utf8'),
         DEFAULT_OPTIONS,
       );
 
@@ -620,10 +653,12 @@ describe('loadParcelConfig', () => {
         {
           packageName: 'parcel-transformer-sub',
           resolveFrom: subConfigFilePath,
+          keyPath: '/transformers/*.js/0',
         },
         {
           packageName: 'parcel-transformer-base',
           resolveFrom: configFilePath,
+          keyPath: '/transformers/*.js/0',
         },
         '...',
       ]);
@@ -652,7 +687,7 @@ describe('loadParcelConfig', () => {
 
       // $FlowFixMe
       await assert.rejects(
-        () => readAndProcessConfigChain(configFilePath, DEFAULT_OPTIONS),
+        () => parseAndProcessConfig(configFilePath, code, DEFAULT_OPTIONS),
         {
           name: 'Error',
           diagnostics: [
@@ -676,6 +711,134 @@ describe('loadParcelConfig', () => {
         },
       );
     });
+
+    it('should emit a codeframe when an extended parcel config file is not found', async () => {
+      let configFilePath = path.join(
+        __dirname,
+        'fixtures',
+        'config-extends-not-found',
+        '.parcelrc',
+      );
+      let code = await DEFAULT_OPTIONS.inputFS.readFile(configFilePath, 'utf8');
+
+      // $FlowFixMe
+      await assert.rejects(
+        () => parseAndProcessConfig(configFilePath, code, DEFAULT_OPTIONS),
+        {
+          name: 'Error',
+          diagnostics: [
+            {
+              message: 'Cannot find extended parcel config',
+              origin: '@parcel/core',
+              filePath: configFilePath,
+              language: 'json5',
+              codeFrame: {
+                code,
+                codeHighlights: [
+                  {
+                    message:
+                      '"./.parclrc-node-modules" does not exist, did you mean "./.parcelrc-node-modules"?',
+                    start: {line: 2, column: 14},
+                    end: {line: 2, column: 38},
+                  },
+                ],
+              },
+            },
+          ],
+        },
+      );
+    });
+
+    it('should emit a codeframe when an extended parcel config node module is not found', async () => {
+      let configFilePath = path.join(
+        __dirname,
+        'fixtures',
+        'config-extends-not-found',
+        '.parcelrc-node-modules',
+      );
+      let code = await DEFAULT_OPTIONS.inputFS.readFile(configFilePath, 'utf8');
+
+      // $FlowFixMe
+      await assert.rejects(
+        () => parseAndProcessConfig(configFilePath, code, DEFAULT_OPTIONS),
+        {
+          name: 'Error',
+          diagnostics: [
+            {
+              message: 'Cannot find extended parcel config',
+              origin: '@parcel/core',
+              filePath: configFilePath,
+              language: 'json5',
+              codeFrame: {
+                code,
+                codeHighlights: [
+                  {
+                    message:
+                      'Cannot find module "@parcel/config-deflt", did you mean "@parcel/config-default"?',
+                    start: {line: 2, column: 14},
+                    end: {line: 2, column: 35},
+                  },
+                ],
+              },
+            },
+          ],
+        },
+      );
+    });
+
+    it('should emit multiple codeframes when multiple extended configs are not found', async () => {
+      let configFilePath = path.join(
+        __dirname,
+        'fixtures',
+        'config-extends-not-found',
+        '.parcelrc-multiple',
+      );
+      let code = await DEFAULT_OPTIONS.inputFS.readFile(configFilePath, 'utf8');
+
+      // $FlowFixMe
+      await assert.rejects(
+        () => parseAndProcessConfig(configFilePath, code, DEFAULT_OPTIONS),
+        {
+          name: 'Error',
+          diagnostics: [
+            {
+              message: 'Cannot find extended parcel config',
+              origin: '@parcel/core',
+              filePath: configFilePath,
+              language: 'json5',
+              codeFrame: {
+                code,
+                codeHighlights: [
+                  {
+                    message:
+                      'Cannot find module "@parcel/config-deflt", did you mean "@parcel/config-default"?',
+                    start: {line: 2, column: 15},
+                    end: {line: 2, column: 36},
+                  },
+                ],
+              },
+            },
+            {
+              message: 'Cannot find extended parcel config',
+              origin: '@parcel/core',
+              filePath: configFilePath,
+              language: 'json5',
+              codeFrame: {
+                code,
+                codeHighlights: [
+                  {
+                    message:
+                      '"./.parclrc" does not exist, did you mean "./.parcelrc"?',
+                    start: {line: 2, column: 39},
+                    end: {line: 2, column: 50},
+                  },
+                ],
+              },
+            },
+          ],
+        },
+      );
+    });
   });
 
   describe('resolve', () => {
diff --git a/packages/core/core/test/fixtures/config-extends-not-found/.parcelrc b/packages/core/core/test/fixtures/config-extends-not-found/.parcelrc
new file mode 100644
index 000000000..71c5256f4
--- /dev/null
+++ b/packages/core/core/test/fixtures/config-extends-not-found/.parcelrc
@@ -0,0 +1,3 @@
+{
+  "extends": "./.parclrc-node-modules"
+}
diff --git a/packages/core/core/test/fixtures/config-extends-not-found/.parcelrc-multiple b/packages/core/core/test/fixtures/config-extends-not-found/.parcelrc-multiple
new file mode 100644
index 000000000..6ba762f43
--- /dev/null
+++ b/packages/core/core/test/fixtures/config-extends-not-found/.parcelrc-multiple
@@ -0,0 +1,3 @@
+{
+  "extends": ["@parcel/config-deflt", "./.parclrc"]
+}
diff --git a/packages/core/core/test/fixtures/config-extends-not-found/.parcelrc-node-modules b/packages/core/core/test/fixtures/config-extends-not-found/.parcelrc-node-modules
new file mode 100644
index 000000000..7bc6ecdf6
--- /dev/null
+++ b/packages/core/core/test/fixtures/config-extends-not-found/.parcelrc-node-modules
@@ -0,0 +1,3 @@
+{
+  "extends": "@parcel/config-deflt"
+}
diff --git a/packages/core/core/test/fixtures/config-plugin-not-found/.parcelrc b/packages/core/core/test/fixtures/config-plugin-not-found/.parcelrc
new file mode 100644
index 000000000..4a063b9d7
--- /dev/null
+++ b/packages/core/core/test/fixtures/config-plugin-not-found/.parcelrc
@@ -0,0 +1,6 @@
+{
+  "extends": "@parcel/config-default",
+  "transformers": {
+    "*.js": ["@parcel/transformer-jj"]
+  }
+}