Repository: parcel-bundler/parcel (js)

Issue:
Parcel 2 - TS declaration files are not generated
<!---
Thanks for filing an issue 😄 ! Before you submit, please read the following:

Search open/closed issues before submitting since someone might have asked the same thing before!
-->

# 🐛 bug report

<!--- Provide a general summary of the issue here -->

Configuring the `types` in `package.json` does not cause Parcel to generate declaration files.
Worth mentioning I am testing parcel in Yarn workspace. This might work in "one package" repository - I did not test that.

## 🎛 Configuration (.babelrc, package.json, cli command)

<!--- If describing a bug, tell us what your babel configuration looks like -->

```json
{
  "main": "./dist/index.js",
  "source": "./src/index.ts",
  "types": "./dist/index.d.ts"
}
```

## 🤔 Expected Behavior

<!--- Tell us what should happen -->

Parcel should use the configured transformer to generate declaration files.

## 😯 Current Behavior

<!--- Tell us what happens instead of the expected behavior -->

<!--- If you are seeing an error, please include the full error message and stack trace -->

The build simply fails:

```
@parcel/namer-default: Target "types" declares an output file path of "dist/index.d.ts" which does not match the compiled bundle type "js".
lit-element-monorepo/packages/first-page/package.json:5:12
  4 |   "source": "./src/index.ts",
> 5 |   "types": "./dist/index.d.ts",
>   |            ^^^^^^^^^^^^^^^^^^^ Did you mean "dist/index.d.js"?
```

_Note: for another package it succeeds building, but doesn't produce the declaration files._

## 🔦 Context

<!--- How has this issue affected you? What are you trying to accomplish? -->

<!--- Providing context helps us come up with a solution that is most useful in the real world -->

In workspaces environment all the packages get linked to the `node_modules` in the root of the repository.
TypeScript expects the library to provide own declaration files, or having the declarations in `@types/library-name`. However with parcel not generating any this is a problem even before publishing the package to npm registry, because typescript is correctly failing in dev-time too:

```
Could not find a declaration file for module `first-page` ...
```

_Question: how are parcel & TypeScript supposed to work in this situation when we're building the package using the configured `sources` and not the pre-built bundle from the `dist`? The imports consistently fail with `Cannot find module 'first-page' or its corresponding type declarations.`. New issue?_

## 💻 Code Sample

<!-- Please provide a code repository, gist, code snippet or sample files to reproduce the issue -->

- clone [this](https://github.com/klemenoslaj/lit-element-monorepo) repository
- checkout `no-typescript-declarations` branch
- execute `yarn workspace first-page build` - failing with error
- execute `yarn workspace second-page build` - builds but without declarations

_To reproduce the imports like failing in the question in previous section, comment out `@ts-ignore` from `packages/application/src/app.element.ts` lines `92` and `97`._

## 🌍 Your Environment

<!--- Include as many relevant details about the environment you experienced the bug in -->

| Software         | Version(s) |
| ---------------- | ---------- |
| Parcel           | 2.0.0-nightly.275+d33fcd9c
| Node             | v12.16.1
| npm/Yarn         | 1.22.4
| Operating System | macOS Mojave 10.14.6

<!-- Love parcel? Please consider supporting our collective:
👉  https://opencollective.com/parcel/donate -->


Pull request description:
Named pipelines should have precedence to other pipelines
# ↪️ Pull Request

The entry asset for `types: "dist/index.d.ts"` was being transformed with the `*.ts` pipeline in a custom parcelrc.

Closes #4666:

Only happens with a custom parcelrc that overrides the `ts` pipeline because when determining the pipeline for `types:index.ts`, the custom parcelrc takes precedence and it is processed like any other ts asset.

The current logic is:
1. select pipeline with matching named pipeline and type
2. otherwise select pipeline with matching type
3. otherwise continue search in parent config

Should be:
1. select pipeline with matching named pipeline and type
2. otherwise select pipeline with matching named pipeline and type in nearest parent config
3. otherwise select pipeline with matching type in current config
4. otherwise select pipeline with matching type in nearest parent config

The flattened parcelrc (custom + default) has these transformers:
```js
 {
  '*.{ts,tsx}': [
    {
      packageName: '@parcel/transformer-typescript-tsc',
      resolveFrom: '/Users/niklas/Desktop/asd/.parcelrc'
    }
  ],
  // custom pipelines above, default pipelines below
  'types:*.{ts,tsx}': [
    {
      packageName: '@parcel/transformer-typescript-types',
      resolveFrom: '/Users/niklas/Desktop/asd/node_modules/@parcel/config-default/index.json'
    }
  ],
  'bundle-text:*': [
    {
      packageName: '@parcel/transformer-inline-string',
      resolveFrom: '/Users/niklas/Desktop/asd/node_modules/@parcel/config-default/index.json'
    },
    '...'
  ],
  'data-url:*': [
  ...
```

So it looks like this call needs to always keep named pipelines on top:
https://github.com/parcel-bundler/parcel/blob/a008be3991c8fd3dcae6689b28ef4fb6bf7c1f06/packages/core/core/src/loadParcelConfig.js#L262-L266

Patch:
diff --git a/packages/core/core/src/loadParcelConfig.js b/packages/core/core/src/loadParcelConfig.js
index 1130cb73e..88eff6d24 100644
--- a/packages/core/core/src/loadParcelConfig.js
+++ b/packages/core/core/src/loadParcelConfig.js
@@ -25,6 +25,8 @@ import assert from 'assert';
 import ParcelConfig from './ParcelConfig';
 import ParcelConfigSchema from './ParcelConfig.schema';
 
+const NAMED_PIPELINE_REGEX = /^[\w-.+]+:/;
+
 type ConfigMap<K, V> = {[K]: V, ...};
 
 export default async function loadParcelConfig(options: ParcelOptions) {
@@ -263,6 +265,7 @@ export function mergeConfigs(
         base.transformers,
         ext.transformers,
         mergePipelines,
+        true,
       ),
       validators: mergeMaps(base.validators, ext.validators, mergePipelines),
       bundler: ext.bundler || base.bundler,
@@ -314,10 +317,11 @@ export function mergePipelines(
   return ext;
 }
 
-export function mergeMaps<K, V>(
+export function mergeMaps<K: string, V>(
   base: ?ConfigMap<K, V>,
   ext: ?ConfigMap<K, V>,
   merger?: (a: V, b: V) => V,
+  hasNamedPipelines: boolean = false,
 ): ConfigMap<K, V> {
   if (!ext) {
     return base || {};
@@ -327,8 +331,29 @@ export function mergeMaps<K, V>(
     return ext;
   }
 
-  // Add the extension options first so they have higher precedence in the output glob map
   let res: ConfigMap<K, V> = {};
+  if (hasNamedPipelines) {
+    // in res, all named pipelines should come before the other pipelines
+    for (let k in ext) {
+      // Flow doesn't correctly infer the type. See https://github.com/facebook/flow/issues/1736.
+      let key: K = (k: any);
+      if (NAMED_PIPELINE_REGEX.test(key)) {
+        res[key] = merger && base[key] ? merger(base[key], ext[key]) : ext[key];
+      }
+    }
+
+    // Add base options that aren't defined in the extension
+    for (let k in base) {
+      let key: K = (k: any);
+      if (NAMED_PIPELINE_REGEX.test(key)) {
+        if (!res[key]) {
+          res[key] = base[key];
+        }
+      }
+    }
+  }
+
+  // Add the extension options first so they have higher precedence in the output glob map
   for (let k in ext) {
     // Flow doesn't correctly infer the type. See https://github.com/facebook/flow/issues/1736.
     let key: K = (k: any);


Test patch:
diff --git a/packages/core/core/test/loadParcelConfig.test.js b/packages/core/core/test/loadParcelConfig.test.js
index 001417fd4..4fc94fdbd 100644
--- a/packages/core/core/test/loadParcelConfig.test.js
+++ b/packages/core/core/test/loadParcelConfig.test.js
@@ -436,14 +436,23 @@ describe('loadParcelConfig', () => {
     });
 
     it('should ensure that extension properties have a higher precidence than base properties', () => {
-      assert.deepEqual(
-        mergeMaps({'*.{js,jsx}': 'base-js'}, {'*.js': 'ext-js'}),
-        {'*.js': 'ext-js', '*.{js,jsx}': 'base-js'},
-      );
-      assert.deepEqual(
-        Object.keys(mergeMaps({'*.{js,jsx}': 'base-js'}, {'*.js': 'ext-js'})),
-        ['*.js', '*.{js,jsx}'],
+      let merged = mergeMaps({'*.{js,jsx}': 'base-js'}, {'*.js': 'ext-js'});
+      assert.deepEqual(merged, {'*.js': 'ext-js', '*.{js,jsx}': 'base-js'});
+      assert.deepEqual(Object.keys(merged), ['*.js', '*.{js,jsx}']);
+    });
+
+    it('should ensure that named pipelines have a higher precidence than extension properties', () => {
+      let merged = mergeMaps(
+        {'types:*.ts': 'base-types-ts'},
+        {'*.ts': 'ext-ts'},
+        undefined,
+        true,
       );
+      assert.deepEqual(merged, {
+        'types:*.ts': 'base-types-ts',
+        '*.ts': 'ext-ts',
+      });
+      assert.deepEqual(Object.keys(merged), ['types:*.ts', '*.ts']);
     });
 
     it('should call a merger function if provided', () => {
diff --git a/packages/core/integration-tests/test/integration/typescript-types-parcelrc/.parcelrc b/packages/core/integration-tests/test/integration/typescript-types-parcelrc/.parcelrc
new file mode 100644
index 000000000..796319e89
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/typescript-types-parcelrc/.parcelrc
@@ -0,0 +1,8 @@
+{
+    "extends": "@parcel/config-default",
+    "transformers": {
+      "*.ts": [
+        "@parcel/transformer-typescript-tsc"
+      ]
+    }
+  }
diff --git a/packages/core/integration-tests/test/integration/typescript-types-parcelrc/index.ts b/packages/core/integration-tests/test/integration/typescript-types-parcelrc/index.ts
new file mode 100644
index 000000000..2527cb398
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/typescript-types-parcelrc/index.ts
@@ -0,0 +1,5 @@
+export class Foo {
+  run(){
+    return "bar";
+  }
+}
diff --git a/packages/core/integration-tests/test/integration/typescript-types-parcelrc/package.json b/packages/core/integration-tests/test/integration/typescript-types-parcelrc/package.json
new file mode 100644
index 000000000..e6aca0899
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/typescript-types-parcelrc/package.json
@@ -0,0 +1,6 @@
+{
+    "name": "typescript-types-parcelrc",
+    "main": "./dist/index.js",
+    "types": "./dist/index.d.ts",
+    "private": true
+}
diff --git a/packages/core/integration-tests/test/integration/typescript-types-parcelrc/yarn.lock b/packages/core/integration-tests/test/integration/typescript-types-parcelrc/yarn.lock
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/core/integration-tests/test/typescript-tsc.js b/packages/core/integration-tests/test/typescript-tsc.js
index 607027b39..352708555 100644
--- a/packages/core/integration-tests/test/typescript-tsc.js
+++ b/packages/core/integration-tests/test/typescript-tsc.js
@@ -1,6 +1,12 @@
 import assert from 'assert';
 import path from 'path';
-import {bundle, run, distDir, outputFS} from '@parcel/test-utils';
+import {
+  assertBundles,
+  bundle,
+  run,
+  distDir,
+  outputFS,
+} from '@parcel/test-utils';
 import {readFileSync} from 'fs';
 
 const configPath = path.join(
@@ -39,4 +45,24 @@ describe('typescript tsc', function() {
     let js = await outputFS.readFile(path.join(distDir, 'index.js'), 'utf8');
     assert(!js.includes('/* test comment */'));
   });
+
+  it('should produce a type declaration file when overriding the ts pipeline', async function() {
+    let b = await bundle(
+      path.join(__dirname, '/integration/typescript-types-parcelrc/index.ts'),
+    );
+
+    assertBundles(b, [
+      {
+        name: 'index.js',
+        assets: ['index.ts'],
+      },
+      {
+        name: 'index.d.ts',
+        assets: ['index.ts'],
+      },
+    ]);
+
+    let output = await run(b);
+    assert.equal(new output.Foo().run(), 'bar');
+  });
 });