Parcel: Parcel 2 fails to bundle TypeScript code that re-exports types

Created on 24 Jun 2020  ยท  12Comments  ยท  Source: parcel-bundler/parcel

๐Ÿ› bug report

Now that Parcel 2 is out of alpha, I thought I'd try adding it to esbuild's bundler performance benchmarks. However, Parcel 2 currently fails to build both of the benchmarks. This issue is about the TypeScript benchmark.

๐ŸŽ› Configuration (.babelrc, package.json, cli command)

Here is how to reproduce the issue:

git clone https://github.com/evanw/esbuild
cd esbuild
make bench-rome-parcel2

๐Ÿค” Expected Behavior

I expected Parcel 2 to be able to build my TypeScript benchmark. Parcel 1 can build the benchmark fine. The benchmark is a copy of the Rome bundler source code.

๐Ÿ˜ฏ Current Behavior

My first attempt at building this benchmark with Parcel 2 failed immediately with a TypeScript parse error:

๐Ÿšจ Build failed.
@parcel/transformer-babel: /Users/evan/dev/esbuild/parcel2/bench/rome/src/@romejs/core/client/Client.ts: `import =` is not supported by @babel/plugin-transform-typescript
Please consider using `import <moduleName> from '<moduleName>';` alongside Typescript's --allowSyntheticDefaultImports option.
  32 | import {PartialMasterQueryRequest} from '../common/bridges/MasterBridge';
  33 | import {loadUserConfig, UserConfig} from '../common/userConfig';
> 34 | import stream = require('stream');
     | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  35 |
  36 | import net = require('net');
  37 |

I did some digging and found #2023, which says that while Parcel 1 uses the TypeScript compiler to parse TypeScript files, Parcel 2 will use Babel instead. And it looks like Babel's support for TypeScript syntax is incomplete. I implemented the suggested workaround of using @parcel/transformer-typescript-tsc instead:

{
  "extends": ["@parcel/config-default"],
  "transformers": {
    "*.ts": ["@parcel/transformer-typescript-tsc"]
  }
}

That let Parcel 2 successfully complete the parsing phase. However, it then failed at the bundling stage:

๐Ÿšจ Build failed.
@parcel/packager-js: bench/rome/src/@romejs/js-parser/tokenizer/index.ts does not export 'Token'
bench/rome/src/@romejs/js-parser/index.ts:8:10
  7 |
> 8 | import {Program} from '@romejs/js-ast';
>   |          ^^^^^
  9 | import {
  10 |   JSParserUserOptions,

Minor bug: the error location is incorrect. The error is on line 14 (not shown) instead of on line 8. Line 14 looks like this:

import {Token} from './tokenizer/index';

Later on in the file is this line:

export {Token};

The ./tokenizer/index file contains this:

export type Token = {
  type: TokenTypes;
  start: Number0;
  end: Number0;
  loc: SourceLocation;
};

I assume the problem is that @parcel/transformer-typescript-tsc compiles each file independently so it doesn't know that Token is a type when processing @romejs/js-parser/index.ts but does know it's a type when processing @romejs/js-parser/tokenizer/index.ts. And that the re-export statement is triggering the bundler to assume Token is a value, not a type, which causes the error.

๐Ÿ’ Possible Solution

This was pretty annoying to handle in esbuild. I ended up handling it by ignoring errors due to missing imports from TypeScript files if the import is re-exported. This is done in this file if you're curious.

๐ŸŒ Your Environment

| Software | Version(s) |
| ---------------- | ---------- |
| Parcel | parcel 2.0.0-beta.1
| Node | node v12.16.2
| npm/Yarn | npm 6.14.5
| Operating System | macOS 10.14.6

Bug TypeScript โœจ Parcel 2 ๐ŸŒณ Tree Shaking

Most helpful comment

I think we're gonna change this to be a warning. Unfortunately, there's no way for Parcel to tell at a per-file level whether a symbol is a type or a value. The import type/export type syntax that they recently added should help with this, but probably not on existing codebases.

All 12 comments

I just came across #4240 which is about this same type re-export problem. It's for having a better error message instead of fixing the issue, so they are somewhat different. Just posting this to cross-link these issues.

I think we're gonna change this to be a warning. Unfortunately, there's no way for Parcel to tell at a per-file level whether a symbol is a type or a value. The import type/export type syntax that they recently added should help with this, but probably not on existing codebases.

there's no way for Parcel to tell at a per-file level whether a symbol is a type or a value.

Actually, with bottom-to-top symbol propagation, we could ๐Ÿ˜‰

Is there a workaround?

--no-scope-hoist but that's not ideal

Then I encounter new error.

Screenshot 2020-07-14 at 10 39 26

I don't even know what file is it.

You need to to configure Babel to transpile optional chaining: https://github.com/terser/terser/issues/567#issuecomment-642897045

We try to use sourcemaps to figure out where it came from, apparently that failed in thiscase.

@grimalschi - I think the best way to work around this issue is to use a new feature in Typescript 3.8 that allow you to explicitly say that you want to import/export a thing as a type, not a value. So if you have a file that is importing a type (not a value) from an index file, instead of writing:

import {Token} from './tokenizer/index';

instead write:

import type {Token} from './tokenizer/index';

This way, typescript (and babel) knows it's a type that's not supposed to be there at runtime, so it will strip the import from the emitted javascript.

I wasn't able to figure out how to get a repro on the original issue, so I'm not 100% sure that this will work in that case, but I'm pretty sure that this is what I've done in the past in similar situations.

The problem occurs if that type is then (re)exported: https://github.com/romefrontend/rome/blob/98d536d87eccb39e5aa8f058ecd9d524aca61a5f/packages/%40romefrontend/js-parser/index.ts#L65 (and you're trying to bundle this file as library)

@astegmaier export type brings new error

Screenshot 2020-07-17 at 17 59 42

Now my workaround is to re-define same type instead of re-export it ยฏ\_(ใƒ„)_/ยฏ

This seems to be fixed after https://github.com/parcel-bundler/parcel/pull/4861:

// a.ts
import { foo } from "./b";
console.log(foo);

// b.ts
import { Token } from "./c";
export { Token };
export const foo = 1;

// c.ts
export type Token = {
    type: number;
};

(But I wasn't able to build Rome because of their strange monorepo setup...)

Thanks for following up on this!

I gave building Rome a shot after the latest update. The monorepo setup works without any changes for tools that respect paths and baseUrl in tsconfig.json. I've been working around this for Parcel 1 and 2 by adding a custom alias to package.json.

So I can actually build Rome with Parcel 2 now. And I finally figured out how to get Parcel 2 to build for node so the Rome bundle now runs successfully! Turns out I needed to add "engines": { "node": "0.0.0" } in package.json since Parcel 1's --target node option doesn't work with Parcel 2.

Here's what I get when I run my TypeScript benchmark (the Rome code):

| | Time | Size |
|---|---|---|
| [email protected] | 18.34s | 1.56mb |
| [email protected] | 55.36s | 1.68mb |

This is mostly with default settings except that Parcel 2 uses @parcel/[email protected] (the Babel TypeScript transpiler that Parcel 2 uses has some bugs such as potentially https://github.com/babel/babel/issues/10015).

I'm not sure why the times and sizes for Parcel 2 are so different than Parcel 1 because I thought Parcel 1 was also using TypeScript's transpiler. Hopefully I didn't mess up something about the configuration.

Was this page helpful?
0 / 5 - 0 ratings