Repository: parcel-bundler/parcel (js)

Issue:
Parcel 2: multi entry/target builds
Split out of #2574.

For SSR, it would be useful to support multiple targets with different entries simultaneously.

Three options came up in the previous issue:

## Explicit entries per target

```js
const parcel = new Parcel({
  entries: {
    browserEntry_page1: '/path/to/browser/entry/of/page1.js',
    browserEntry_page2: '/path/to/browser/entry/of/page2.js',
    serverEntry: '/path/to/server.js',
  },

  targets: {
    browserEntry_page1: {
      "browsers": ["> 1%", "not dead"]
    },
    browserEntry_page2: {
      "browsers": ["> 1%", "not dead"]
    },
    serverEntry: {
      "node": ["^8.0.0"]
    },
  }
});
```

## An array of parcel options

```js
const parcel = new Parcel([
  {
    entries: ['page1.js', 'page2.js']
    targets: {
      browser: {
        browser: ['>1%', 'not dead']
      }
    }
  },
  {
    entries: ['server.js']
    targets: {
      node: {
        node: ['^8.0.0']
      }
    }
  }
});
```

## Multiple Parcel instances, sharing a worker farm

We were thinking of making the worker farm an option anyway, so this might work for free. By sharing a worker farm instance, multiple Parcel instances could run in parallel.

```js
const workerFarm = new WorkerFarm();

const browserParcel = new Parcel({
  workerFarm,
  entries: ['page1.js', 'page2.js']
  targets: {
    browser: {
      browser: ['>1%', 'not dead']
    }
  }
});

const serverParcel = new Parcel({
  workerFarm,
  entries: ['server.js']
  targets: {
    node: {
      node: ['^8.0.0']
    }
  }
});
```

Thoughts?

cc. @brillout @padmaia @wbinnssmith 

Pull request description:
Support for a custom source per target with same semantics as package (second try) #source #5824
# ↪️ Pull Request

This provides a support for multi-entry builds where a given target has it's own entry point specified.

This is accomplished by adding a `source` property as an option on each target specified in package.json targets.

Any target with a specific source specified will only be built with that source, ignoring all other entries.

Resolves #3302

PR to update website documentation is here: https://github.com/parcel-bundler/website/pull/814

## 💻 Examples

    "source": ["one.js", "two.js"],
    "targets": {
        "a": {
        },
        "b" {
          "source": "three.js"
        }
      }

In this example, the new `source` is specified for target *b*. As a result, source one and two are only built for target *a* and target *b* is only built with source three.

## 🚨 Test instructions

I added example 'target-source' to the examples. It compiles two packages simultaneously, both of which use a source override.

I also added new unit tests for the EntryRequest and TargetRequest.

## ✔️ PR Todo

- [x] Added/updated unit tests for this change
- [x] Filled out test instructions (In case there aren't any unit tests)
- [x] Included links to related issues/PRs


Patch:
diff --git a/packages/core/core/src/TargetDescriptor.schema.js b/packages/core/core/src/TargetDescriptor.schema.js
index 016643654..02d93b5ff 100644
--- a/packages/core/core/src/TargetDescriptor.schema.js
+++ b/packages/core/core/src/TargetDescriptor.schema.js
@@ -72,6 +72,17 @@ export const PACKAGE_DESCRIPTOR_SCHEMA: SchemaObject = {
     isLibrary: {
       type: 'boolean',
     },
+    source: {
+      oneOf: [
+        {
+          type: 'string',
+        },
+        {
+          type: 'array',
+          items: {type: 'string'},
+        },
+      ],
+    },
     sourceMap: {
       oneOf: [
         {
diff --git a/packages/core/core/src/requests/EntryRequest.js b/packages/core/core/src/requests/EntryRequest.js
index 5d26a5676..7b2fc6f68 100644
--- a/packages/core/core/src/requests/EntryRequest.js
+++ b/packages/core/core/src/requests/EntryRequest.js
@@ -1,8 +1,9 @@
 // @flow strict-local
 
-import type {Async, FilePath, File} from '@parcel/types';
+import type {Async, FilePath, File, PackageJSON} from '@parcel/types';
 import type {StaticRunOpts} from '../RequestTracker';
 import type {Entry, ParcelOptions} from '../types';
+import type {FileSystem} from '@parcel/fs';
 
 import {isDirectoryInside, isGlob, glob} from '@parcel/utils';
 import ThrowableDiagnostic, {md} from '@parcel/diagnostic';
@@ -63,7 +64,34 @@ async function run({input, api, options}: RunOpts): Promise<EntryResult> {
   return result;
 }
 
-class EntryResolver {
+async function assertFile(
+  fs: FileSystem,
+  source: string,
+  diagnosticPath: string,
+) {
+  let stat;
+  try {
+    stat = await fs.stat(source);
+  } catch (err) {
+    throw new ThrowableDiagnostic({
+      diagnostic: {
+        message: `${diagnosticPath} does not exist`,
+        filePath: source,
+      },
+    });
+  }
+
+  if (!stat.isFile()) {
+    throw new ThrowableDiagnostic({
+      diagnostic: {
+        message: `${diagnosticPath} is not a file`,
+        filePath: source,
+      },
+    });
+  }
+}
+
+export class EntryResolver {
   options: ParcelOptions;
 
   constructor(options: ParcelOptions) {
@@ -102,42 +130,57 @@ class EntryResolver {
 
     if (stat.isDirectory()) {
       let pkg = await this.readPackage(entry);
-      if (pkg && pkg.source != null) {
-        let entries = [];
-        let files = [];
-
-        let pkgSources = Array.isArray(pkg.source) ? pkg.source : [pkg.source];
-        for (let pkgSource of pkgSources) {
-          if (typeof pkgSource === 'string') {
-            let source = path.join(path.dirname(pkg.filePath), pkgSource);
-            try {
-              stat = await this.options.inputFS.stat(source);
-            } catch (err) {
-              throw new ThrowableDiagnostic({
-                diagnostic: {
-                  message: md`${pkgSource} in ${path.relative(
-                    this.options.inputFS.cwd(),
-                    pkg.filePath,
-                  )}#source does not exist`,
-                  filePath: source,
-                },
-              });
-            }
 
-            if (!stat.isFile()) {
-              throw new ThrowableDiagnostic({
-                diagnostic: {
-                  message: md`${pkgSource} in ${path.relative(
-                    this.options.inputFS.cwd(),
-                    pkg.filePath,
-                  )}#source is not a file`,
+      if (pkg) {
+        let {filePath} = pkg;
+        let entries = [];
+        let files = [{filePath}];
+
+        let targetsWithSources = 0;
+        if (pkg.targets) {
+          for (let targetName in pkg.targets) {
+            let target = pkg.targets[targetName];
+            if (target.source != null) {
+              targetsWithSources++;
+              let targetSources = Array.isArray(target.source)
+                ? target.source
+                : [target.source];
+              for (let relativeSource of targetSources) {
+                let source = path.join(entry, relativeSource);
+                let diagnosticPath = md`${relativeSource} in ${path.relative(
+                  this.options.inputFS.cwd(),
+                  filePath,
+                )}#targets["${targetName}"].source`;
+                await assertFile(this.options.inputFS, source, diagnosticPath);
+
+                entries.push({
                   filePath: source,
-                },
-              });
+                  packagePath: entry,
+                  target: targetName,
+                });
+              }
             }
+          }
+        }
 
+        let allTargetsHaveSource =
+          targetsWithSources > 0 &&
+          pkg != null &&
+          pkg.targets != null &&
+          Object.keys(pkg.targets).length === targetsWithSources;
+
+        if (!allTargetsHaveSource && pkg.source != null) {
+          let pkgSources = Array.isArray(pkg.source)
+            ? pkg.source
+            : [pkg.source];
+          for (let pkgSource of pkgSources) {
+            let source = path.join(path.dirname(filePath), pkgSource);
+            let diagnosticPath = md`${pkgSource} in ${path.relative(
+              this.options.inputFS.cwd(),
+              filePath,
+            )}#source`;
+            await assertFile(this.options.inputFS, source, diagnosticPath);
             entries.push({filePath: source, packagePath: entry});
-            files.push({filePath: pkg.filePath});
           }
         }
 
@@ -179,7 +222,9 @@ class EntryResolver {
     });
   }
 
-  async readPackage(entry: FilePath) {
+  async readPackage(
+    entry: FilePath,
+  ): Promise<?{...PackageJSON, filePath: FilePath, ...}> {
     let content, pkg;
     let pkgFile = path.join(entry, 'package.json');
     try {
@@ -202,7 +247,6 @@ class EntryResolver {
       });
     }
 
-    pkg.filePath = pkgFile;
-    return pkg;
+    return {...pkg, filePath: pkgFile};
   }
 }
diff --git a/packages/core/core/src/requests/TargetRequest.js b/packages/core/core/src/requests/TargetRequest.js
index fc43af1e9..88359ef1f 100644
--- a/packages/core/core/src/requests/TargetRequest.js
+++ b/packages/core/core/src/requests/TargetRequest.js
@@ -70,12 +70,26 @@ export default function createTargetRequest(input: Entry): TargetRequest {
   };
 }
 
+export function skipTarget(
+  targetName: string,
+  exclusiveTarget?: FilePath,
+  descriptorSource?: FilePath | Array<FilePath>,
+): boolean {
+  //  We skip targets if they have a descriptor.source and don't match the current exclusiveTarget
+  //  They will be handled by a separate resolvePackageTargets call from their Entry point
+  //  but with exclusiveTarget set.
+
+  return exclusiveTarget == null
+    ? descriptorSource != null
+    : targetName !== exclusiveTarget;
+}
+
 async function run({input, api, options}: RunOpts) {
   let targetResolver = new TargetResolver(
     api,
     optionsProxy(options, api.invalidateOnOptionChange),
   );
-  let targets = await targetResolver.resolve(input.packagePath);
+  let targets = await targetResolver.resolve(input.packagePath, input.target);
 
   let configResult = nullthrows(
     await api.runRequest<null, ConfigAndCachePath>(createParcelConfigRequest()),
@@ -104,10 +118,19 @@ export class TargetResolver {
     this.options = options;
   }
 
-  async resolve(rootDir: FilePath): Promise<Array<Target>> {
+  async resolve(
+    rootDir: FilePath,
+    exclusiveTarget?: string,
+  ): Promise<Array<Target>> {
     let optionTargets = this.options.targets;
+    if (exclusiveTarget != null && optionTargets == null) {
+      optionTargets = [exclusiveTarget];
+    }
 
-    let packageTargets = await this.resolvePackageTargets(rootDir);
+    let packageTargets = await this.resolvePackageTargets(
+      rootDir,
+      exclusiveTarget,
+    );
     let targets: Array<Target>;
     if (optionTargets) {
       if (Array.isArray(optionTargets)) {
@@ -137,62 +160,77 @@ export class TargetResolver {
       } else {
         // Otherwise, it's an object map of target descriptors (similar to those
         // in package.json). Adapt them to native targets.
-        targets = Object.entries(optionTargets).map(([name, _descriptor]) => {
-          let {distDir, ...descriptor} = parseDescriptor(
-            name,
-            _descriptor,
-            null,
-            JSON.stringify({targets: optionTargets}, null, '\t'),
-          );
-          if (distDir == null) {
-            let optionTargetsString = JSON.stringify(optionTargets, null, '\t');
-            throw new ThrowableDiagnostic({
-              diagnostic: {
-                message: md`Missing distDir for target "${name}"`,
-                origin: '@parcel/core',
-                codeFrame: {
-                  code: optionTargetsString,
-                  codeHighlights: generateJSONCodeHighlights(
-                    optionTargetsString,
-                    [
-                      {
-                        key: `/${name}`,
-                        type: 'value',
-                      },
-                    ],
-                  ),
+        targets = Object.entries(optionTargets)
+          .map(([name, _descriptor]) => {
+            let {distDir, ...descriptor} = parseDescriptor(
+              name,
+              _descriptor,
+              null,
+              JSON.stringify({targets: optionTargets}, null, '\t'),
+            );
+            if (distDir == null) {
+              let optionTargetsString = JSON.stringify(
+                optionTargets,
+                null,
+                '\t',
+              );
+              throw new ThrowableDiagnostic({
+                diagnostic: {
+                  message: md`Missing distDir for target "${name}"`,
+                  origin: '@parcel/core',
+                  codeFrame: {
+                    code: optionTargetsString,
+                    codeHighlights: generateJSONCodeHighlights(
+                      optionTargetsString || '',
+                      [
+                        {
+                          key: `/${name}`,
+                          type: 'value',
+                        },
+                      ],
+                    ),
+                  },
                 },
-              },
-            });
-          }
-          let target: Target = {
-            name,
-            distDir: path.resolve(this.fs.cwd(), distDir),
-            publicUrl:
-              descriptor.publicUrl ??
-              this.options.defaultTargetOptions.publicUrl,
-            env: createEnvironment({
-              engines: descriptor.engines,
-              context: descriptor.context,
-              isLibrary: descriptor.isLibrary,
-              includeNodeModules: descriptor.includeNodeModules,
-              outputFormat: descriptor.outputFormat,
-              shouldOptimize:
-                this.options.defaultTargetOptions.shouldOptimize &&
-                descriptor.optimize !== false,
-              shouldScopeHoist:
-                this.options.defaultTargetOptions.shouldScopeHoist &&
-                descriptor.scopeHoist !== false,
-              sourceMap: normalizeSourceMap(this.options, descriptor.sourceMap),
-            }),
-          };
+              });
+            }
+            let target: Target = {
+              name,
+              distDir: path.resolve(this.fs.cwd(), distDir),
+              publicUrl:
+                descriptor.publicUrl ??
+                this.options.defaultTargetOptions.publicUrl,
+              env: createEnvironment({
+                engines: descriptor.engines,
+                context: descriptor.context,
+                isLibrary: descriptor.isLibrary,
+                includeNodeModules: descriptor.includeNodeModules,
+                outputFormat: descriptor.outputFormat,
+                shouldOptimize:
+                  this.options.defaultTargetOptions.shouldOptimize &&
+                  descriptor.optimize !== false,
+                shouldScopeHoist:
+                  this.options.defaultTargetOptions.shouldScopeHoist &&
+                  descriptor.scopeHoist !== false,
+                sourceMap: normalizeSourceMap(
+                  this.options,
+                  descriptor.sourceMap,
+                ),
+              }),
+            };
 
-          if (descriptor.distEntry != null) {
-            target.distEntry = descriptor.distEntry;
-          }
+            if (descriptor.distEntry != null) {
+              target.distEntry = descriptor.distEntry;
+            }
 
-          return target;
-        });
+            if (descriptor.source != null) {
+              target.source = descriptor.source;
+            }
+
+            return target;
+          })
+          .filter(
+            target => !skipTarget(target.name, exclusiveTarget, target.source),
+          );
       }
 
       let serve = this.options.serveOptions;
@@ -241,14 +279,23 @@ export class TargetResolver {
           },
         ];
       } else {
-        targets = Array.from(packageTargets.values());
+        targets = Array.from(packageTargets.values()).filter(descriptor => {
+          return !skipTarget(
+            descriptor.name,
+            exclusiveTarget,
+            descriptor.source,
+          );
+        });
       }
     }
 
     return targets;
   }
 
-  async resolvePackageTargets(rootDir: FilePath): Promise<Map<string, Target>> {
+  async resolvePackageTargets(
+    rootDir: FilePath,
+    exclusiveTarget?: string,
+  ): Promise<Map<string, Target>> {
     let rootFile = path.join(rootDir, 'index');
     let conf = await loadConfig(this.fs, rootFile, ['package.json']);
 
@@ -434,6 +481,10 @@ export class TargetResolver {
           pkgContents,
         );
 
+        if (skipTarget(targetName, exclusiveTarget, descriptor.source)) {
+          continue;
+        }
+
         let isLibrary =
           typeof distEntry === 'string'
             ? path.extname(distEntry) === '.js'
@@ -537,6 +588,9 @@ export class TargetResolver {
           pkgContents,
         );
         let pkgDir = path.dirname(nullthrows(pkgFilePath));
+        if (skipTarget(targetName, exclusiveTarget, descriptor.source)) {
+          continue;
+        }
         targets.set(targetName, {
           name: targetName,
           distDir:
diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js
index 0775b86c1..8a9505146 100644
--- a/packages/core/core/src/types.js
+++ b/packages/core/core/src/types.js
@@ -84,6 +84,7 @@ export type Target = {|
   publicUrl: string,
   loc?: ?SourceLocation,
   pipeline?: string,
+  source?: FilePath | Array<FilePath>,
 |};
 
 export type Dependency = {|
@@ -298,6 +299,7 @@ export type EntrySpecifierNode = {|
 export type Entry = {|
   filePath: FilePath,
   packagePath: FilePath,
+  target?: string,
 |};
 
 export type EntryFileNode = {|
diff --git a/packages/core/types/index.js b/packages/core/types/index.js
index cdc727186..0d2e5bf77 100644
--- a/packages/core/types/index.js
+++ b/packages/core/types/index.js
@@ -133,6 +133,7 @@ export type PackageTargetDescriptor = {|
   +isLibrary?: boolean,
   +optimize?: boolean,
   +scopeHoist?: boolean,
+  +source?: FilePath | Array<FilePath>,
 |};
 
 /**
@@ -233,7 +234,7 @@ export type PackageJSON = {
   module?: FilePath,
   types?: FilePath,
   browser?: FilePath | {[FilePath]: FilePath | boolean, ...},
-  source?: FilePath | {[FilePath]: FilePath, ...},
+  source?: FilePath | Array<FilePath>,
   alias?: {[PackageName | FilePath | Glob]: PackageName | FilePath, ...},
   browserslist?: Array<string> | {[string]: Array<string>},
   engines?: Engines,
diff --git a/packages/utils/node-resolver-core/src/NodeResolver.js b/packages/utils/node-resolver-core/src/NodeResolver.js
index 39e189fc3..d642525ad 100644
--- a/packages/utils/node-resolver-core/src/NodeResolver.js
+++ b/packages/utils/node-resolver-core/src/NodeResolver.js
@@ -799,9 +799,12 @@ export default class NodeResolver {
     if (env.isBrowser()) pkgKeys.push('browser');
 
     for (let pkgKey of pkgKeys) {
-      let alias = await this.getAlias(filename, pkg, pkg[pkgKey]);
-      if (alias != null) {
-        return alias;
+      let pkgKeyValue = pkg[pkgKey];
+      if (!Array.isArray(pkgKeyValue)) {
+        let alias = await this.getAlias(filename, pkg, pkgKeyValue);
+        if (alias != null) {
+          return alias;
+        }
       }
     }
     return null;


Test patch:
diff --git a/packages/core/core/test/EntryRequest.test.js b/packages/core/core/test/EntryRequest.test.js
new file mode 100644
index 000000000..7a1bd7ed9
--- /dev/null
+++ b/packages/core/core/test/EntryRequest.test.js
@@ -0,0 +1,94 @@
+// @flow strict-local
+import assert from 'assert';
+import path from 'path';
+import {inputFS as fs} from '@parcel/test-utils';
+import {EntryResolver} from '../src/requests/EntryRequest';
+import {DEFAULT_OPTIONS as _DEFAULT_OPTIONS} from './test-utils';
+import {md} from '@parcel/diagnostic';
+
+const DEFAULT_OPTIONS = {
+  ..._DEFAULT_OPTIONS,
+  defaultTargetOptions: {
+    ..._DEFAULT_OPTIONS.defaultTargetOptions,
+    sourceMaps: true,
+  },
+};
+
+const INVALID_SOURCE_MISSING_FIXTURE_PATH = path.join(
+  __dirname,
+  'fixtures/invalid-source-missing',
+);
+
+const INVALID_SOURCE_NOT_FILE_FIXTURE_PATH = path.join(
+  __dirname,
+  'fixtures/invalid-source-not-file',
+);
+
+const INVALID_TARGET_SOURCE_MISSING_FIXTURE_PATH = path.join(
+  __dirname,
+  'fixtures/invalid-target-source-missing',
+);
+
+const INVALID_TARGET_SOURCE_NOT_FILE_FIXTURE_PATH = path.join(
+  __dirname,
+  'fixtures/invalid-target-source-not-file',
+);
+
+function packagePath(fixturePath) {
+  return path.join(path.relative(fs.cwd(), fixturePath), '/package.json');
+}
+
+describe('EntryResolver', function() {
+  let entryResolver = new EntryResolver({...DEFAULT_OPTIONS});
+
+  it('rejects missing source in package.json', async function() {
+    this.timeout(10000);
+    // $FlowFixMe assert.rejects is Node 10+
+    await assert.rejects(
+      () => entryResolver.resolveEntry(INVALID_SOURCE_MISSING_FIXTURE_PATH),
+      {
+        message: md`missing.js in ${packagePath(
+          INVALID_SOURCE_MISSING_FIXTURE_PATH,
+        )}#source does not exist`,
+      },
+    );
+  });
+  it('rejects non-file source in package.json', async function() {
+    this.timeout(10000);
+    // $FlowFixMe assert.rejects is Node 10+
+    await assert.rejects(
+      () => entryResolver.resolveEntry(INVALID_SOURCE_NOT_FILE_FIXTURE_PATH),
+      {
+        message: md`src in ${packagePath(
+          INVALID_SOURCE_NOT_FILE_FIXTURE_PATH,
+        )}#source is not a file`,
+      },
+    );
+  });
+  it('rejects missing target source in package.json', async function() {
+    this.timeout(10000);
+    // $FlowFixMe assert.rejects is Node 10+
+    await assert.rejects(
+      () =>
+        entryResolver.resolveEntry(INVALID_TARGET_SOURCE_MISSING_FIXTURE_PATH),
+      {
+        message: md`missing.js in ${packagePath(
+          INVALID_TARGET_SOURCE_MISSING_FIXTURE_PATH,
+        )}#targets["a"].source does not exist`,
+      },
+    );
+  });
+  it('rejects non-file target source in package.json', async function() {
+    this.timeout(10000);
+    // $FlowFixMe assert.rejects is Node 10+
+    await assert.rejects(
+      () =>
+        entryResolver.resolveEntry(INVALID_TARGET_SOURCE_NOT_FILE_FIXTURE_PATH),
+      {
+        message: md`src in ${packagePath(
+          INVALID_TARGET_SOURCE_NOT_FILE_FIXTURE_PATH,
+        )}#targets["a"].source is not a file`,
+      },
+    );
+  });
+});
diff --git a/packages/core/core/test/TargetRequest.test.js b/packages/core/core/test/TargetRequest.test.js
index b24b358d1..eacc92b78 100644
--- a/packages/core/core/test/TargetRequest.test.js
+++ b/packages/core/core/test/TargetRequest.test.js
@@ -429,6 +429,87 @@ describe('TargetResolver', () => {
     );
   });
 
+  it('skips targets with custom entry source for default entry', async () => {
+    let targetResolver = new TargetResolver(api, {
+      ...DEFAULT_OPTIONS,
+      targets: {
+        customA: {
+          context: 'browser',
+          distDir: 'customA',
+          source: 'customA/index.js',
+        },
+        customB: {
+          distDir: 'customB',
+        },
+      },
+    });
+
+    assert.deepEqual(
+      await targetResolver.resolve(COMMON_TARGETS_FIXTURE_PATH),
+      [
+        {
+          name: 'customB',
+          distDir: path.resolve('customB'),
+          publicUrl: '/',
+          env: {
+            id: 'daa5d206066497852a3e8af4ff268cc2',
+            context: 'browser',
+            engines: {
+              browsers: ['> 0.25%'],
+            },
+            includeNodeModules: true,
+            outputFormat: 'global',
+            isLibrary: false,
+            shouldOptimize: false,
+            shouldScopeHoist: false,
+            sourceMap: {},
+          },
+        },
+      ],
+    );
+  });
+
+  it('skips other targets with custom entry', async () => {
+    let targetResolver = new TargetResolver(api, {
+      ...DEFAULT_OPTIONS,
+      targets: {
+        customA: {
+          context: 'browser',
+          distDir: 'customA',
+          source: 'customA/index.js',
+        },
+        customB: {
+          distDir: 'customB',
+        },
+      },
+    });
+
+    assert.deepEqual(
+      await targetResolver.resolve(COMMON_TARGETS_FIXTURE_PATH, 'customA'),
+      [
+        {
+          name: 'customA',
+          distDir: path.resolve('customA'),
+          publicUrl: '/',
+          env: {
+            id: 'daa5d206066497852a3e8af4ff268cc2',
+            context: 'browser',
+            engines: {
+              browsers: ['> 0.25%'],
+            },
+            includeNodeModules: true,
+            outputFormat: 'global',
+            isLibrary: false,
+            shouldOptimize: false,
+            shouldScopeHoist: false,
+            sourceMap: {},
+          },
+          source: 'customA/index.js',
+        },
+      ],
+    );
+  });
+
   it('resolves main target with context from package.json', async () => {
     let targetResolver = new TargetResolver(api, DEFAULT_OPTIONS);
     assert.deepEqual(await targetResolver.resolve(CONTEXT_FIXTURE_PATH), [
diff --git a/packages/core/core/test/fixtures/invalid-source-missing/package.json b/packages/core/core/test/fixtures/invalid-source-missing/package.json
new file mode 100644
index 000000000..2e2f3244d
--- /dev/null
+++ b/packages/core/core/test/fixtures/invalid-source-missing/package.json
@@ -0,0 +1,5 @@
+{
+  "main": "dist/main/index.js",
+  "module": "dist/module/index.js",
+  "source": "missing.js"
+}
\ No newline at end of file
diff --git a/packages/core/core/test/fixtures/invalid-source-not-file/package.json b/packages/core/core/test/fixtures/invalid-source-not-file/package.json
new file mode 100644
index 000000000..19aff7f18
--- /dev/null
+++ b/packages/core/core/test/fixtures/invalid-source-not-file/package.json
@@ -0,0 +1,5 @@
+{
+  "main": "dist/main/index.js",
+  "module": "dist/module/index.js",
+  "source": "src"
+}
\ No newline at end of file
diff --git a/packages/core/core/test/fixtures/invalid-source-not-file/src/index.js b/packages/core/core/test/fixtures/invalid-source-not-file/src/index.js
new file mode 100644
index 000000000..cfa374bdb
--- /dev/null
+++ b/packages/core/core/test/fixtures/invalid-source-not-file/src/index.js
@@ -0,0 +1,1 @@
+// empty
\ No newline at end of file
diff --git a/packages/core/core/test/fixtures/invalid-target-source-missing/package.json b/packages/core/core/test/fixtures/invalid-target-source-missing/package.json
new file mode 100644
index 000000000..4c7cc87bb
--- /dev/null
+++ b/packages/core/core/test/fixtures/invalid-target-source-missing/package.json
@@ -0,0 +1,9 @@
+{
+  "main": "dist/main/index.js",
+  "module": "dist/module/index.js",
+  "targets": {
+    "a": {
+      "source": "missing.js"
+    }
+  }
+}
\ No newline at end of file
diff --git a/packages/core/core/test/fixtures/invalid-target-source-not-file/package.json b/packages/core/core/test/fixtures/invalid-target-source-not-file/package.json
new file mode 100644
index 000000000..a45f47824
--- /dev/null
+++ b/packages/core/core/test/fixtures/invalid-target-source-not-file/package.json
@@ -0,0 +1,9 @@
+{
+  "main": "dist/main/index.js",
+  "module": "dist/module/index.js",
+  "targets": {
+    "a": {
+      "source": "src"
+    }
+  }
+}
\ No newline at end of file
diff --git a/packages/core/core/test/fixtures/invalid-target-source-not-file/src/index.js b/packages/core/core/test/fixtures/invalid-target-source-not-file/src/index.js
new file mode 100644
index 000000000..cfa374bdb
--- /dev/null
+++ b/packages/core/core/test/fixtures/invalid-target-source-not-file/src/index.js
@@ -0,0 +1,1 @@
+// empty
\ No newline at end of file
diff --git a/packages/core/integration-tests/test/integration/target-source/package.json b/packages/core/integration-tests/test/integration/target-source/package.json
new file mode 100644
index 000000000..dea3647bd
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "@parcel/target-source",
+  "version": "2.0.0-beta.1",
+  "license": "MIT",
+  "private": true,
+  "source": "src/index.js",
+  "scripts": {
+    "clean": "rm -rf ../.parcel-cache dist",
+    "demo": "yarn clean && PARCEL_WORKERS=0 parcel build packages/package-a",
+    "debug": "yarn clean && PARCEL_WORKERS=0 node --inspect-brk ../../core/parcel/src/bin.js build packages/*",
+    "watch": "nodemon -w ../../ -i packages/package-a/dist -i packages/package-b/dist -i ../.parcel-cache -d 2 -x yarn demo"
+  }
+}
diff --git a/packages/core/integration-tests/test/integration/target-source/packages/package-a/package.json b/packages/core/integration-tests/test/integration/target-source/packages/package-a/package.json
new file mode 100644
index 000000000..259e7dc1b
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/packages/package-a/package.json
@@ -0,0 +1,28 @@
+{
+  "name": "@parcel/target-source-package-a",
+  "version": "2.0.0-beta.1",
+  "license": "MIT",
+  "private": true,
+  "source": "src/index.js",
+  "devDependencies": {
+    "@parcel/babel-register": "2.0.0-beta.1",
+    "@parcel/core": "2.0.0-beta.1"
+  },
+  "targets": {
+    "alternate": {
+      "source": ["src/indexAlternate.js", "src/indexAlternate2.js"]
+    },
+    "browser": {
+      "engines": {
+        "browsers": [
+          "last 1 Chrome version"
+        ]
+      }
+    }
+  },
+  "dependencies": {
+    "lodash": "^4.17.11",
+    "react": "^16.6.3",
+    "react-dom": "^16.6.3"
+  }
+}
diff --git a/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/bar.js b/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/bar.js
new file mode 100644
index 000000000..83e7413dc
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/bar.js
@@ -0,0 +1,3 @@
+export default function() {
+  return 'hello bar!';
+}
diff --git a/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/foo.js b/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/foo.js
new file mode 100644
index 000000000..83c867ce0
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/foo.js
@@ -0,0 +1,3 @@
+export default function() {
+  return 'hello foo5!';
+}
diff --git a/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/index.js b/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/index.js
new file mode 100644
index 000000000..0ab19b5b6
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/index.js
@@ -0,0 +1,3 @@
+import foo from './foo';
+
+console.log(foo());
diff --git a/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/indexAlternate.js b/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/indexAlternate.js
new file mode 100644
index 000000000..2339ef4dc
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/indexAlternate.js
@@ -0,0 +1,3 @@
+import bar from './bar';
+
+console.log(bar());
diff --git a/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/indexAlternate2.js b/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/indexAlternate2.js
new file mode 100644
index 000000000..0ab19b5b6
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/packages/package-a/src/indexAlternate2.js
@@ -0,0 +1,3 @@
+import foo from './foo';
+
+console.log(foo());
diff --git a/packages/core/integration-tests/test/integration/target-source/packages/package-b/package.json b/packages/core/integration-tests/test/integration/target-source/packages/package-b/package.json
new file mode 100644
index 000000000..8be3234fb
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/packages/package-b/package.json
@@ -0,0 +1,28 @@
+{
+  "name": "@parcel/target-source-package-b",
+  "version": "2.0.0-beta.1",
+  "license": "MIT",
+  "private": true,
+  "source": "src/index.js",
+  "devDependencies": {
+    "@parcel/babel-register": "2.0.0-beta.1",
+    "@parcel/core": "2.0.0-beta.1"
+  },
+  "targets": {
+    "alternate": {
+      "source": "src/indexAlternate.js"
+    },
+    "browser": {
+      "engines": {
+        "browsers": [
+          "last 1 Chrome version"
+        ]
+      }
+    }
+  },
+  "dependencies": {
+    "lodash": "^4.17.11",
+    "react": "^16.6.3",
+    "react-dom": "^16.6.3"
+  }
+}
diff --git a/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/bar.js b/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/bar.js
new file mode 100644
index 000000000..83e7413dc
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/bar.js
@@ -0,0 +1,3 @@
+export default function() {
+  return 'hello bar!';
+}
diff --git a/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/foo.js b/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/foo.js
new file mode 100644
index 000000000..83c867ce0
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/foo.js
@@ -0,0 +1,3 @@
+export default function() {
+  return 'hello foo5!';
+}
diff --git a/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/index.js b/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/index.js
new file mode 100644
index 000000000..0ab19b5b6
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/index.js
@@ -0,0 +1,3 @@
+import foo from './foo';
+
+console.log(foo());
diff --git a/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/indexAlternate.js b/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/indexAlternate.js
new file mode 100644
index 000000000..2339ef4dc
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/target-source/packages/package-b/src/indexAlternate.js
@@ -0,0 +1,3 @@
+import bar from './bar';
+
+console.log(bar());
diff --git a/packages/core/integration-tests/test/integration/target-source/yarn.lock b/packages/core/integration-tests/test/integration/target-source/yarn.lock
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/core/integration-tests/test/monorepos.js b/packages/core/integration-tests/test/monorepos.js
index 696b1eab8..83f613493 100644
--- a/packages/core/integration-tests/test/monorepos.js
+++ b/packages/core/integration-tests/test/monorepos.js
@@ -27,6 +27,70 @@ describe('monorepos', function() {
     }
   });
 
+  it('should compile packages with target source overrides', async function() {
+    let fixture = path.join(__dirname, '/integration/target-source');
+    let oldcwd = inputFS.cwd();
+    inputFS.chdir(fixture);
+
+    try {
+      let b = await bundle(
+        [
+          path.join(fixture, 'packages/package-a'),
+          path.join(fixture, 'packages/package-b'),
+        ],
+        {
+          defaultTargetOptions: {
+            shouldScopeHoist: true,
+            distDir,
+          },
+        },
+      );
+
+      assertBundles(b, [
+        {
+          name: 'index.js',
+          assets: ['foo.js', 'index.js'],
+        },
+        {
+          name: 'indexAlternate.js',
+          assets: ['bar.js', 'indexAlternate.js'],
+        },
+        {
+          name: 'index.js',
+          assets: ['foo.js', 'index.js'],
+        },
+        {
+          name: 'indexAlternate.js',
+          assets: ['bar.js', 'indexAlternate.js'],
+        },
+        {
+          name: 'indexAlternate2.js',
+          assets: ['foo.js', 'indexAlternate2.js'],
+        },
+      ]);
+
+      let contents = await outputFS.readFile(
+        path.join(distDir, '/package-a/src/index.js'),
+        'utf8',
+      );
+      assert(contents.includes('hello foo'));
+
+      contents = await outputFS.readFile(
+        path.join(distDir, '/package-a/src/indexAlternate.js'),
+        'utf8',
+      );
+      assert(contents.includes('hello bar'));
+
+      contents = await outputFS.readFile(
+        path.join(distDir, '/package-a/src/indexAlternate2.js'),
+        'utf8',
+      );
+      assert(contents.includes('hello foo'));
+    } finally {
+      inputFS.chdir(oldcwd);
+    }
+  });
+
   it('should build using root targets with entry files inside packages and cwd at project root', async function() {
     let fixture = path.join(__dirname, '/integration/monorepo');
     let oldcwd = inputFS.cwd();