Parcel: How should I build a library using Parcel?

Created on 24 Jun 2020  ·  8Comments  ·  Source: parcel-bundler/parcel

I'm trying to use Parcel 2 to develop libraries so that:

  1. the source code is in TypeScript, and
  2. I can publish builds that:

    • can be used with TS or JS,

    • run in node or in the browser, and

    • can be used with or without a package manager (e.g. including deps and exporting to global)

In theory, this should be as easy as:

  1. Creating a main target with esmodule and setting "type": "module" in package.json,
  2. a types target, and
  3. a browser target that includes deps and puts exports on globalThis.

However, even after spending hours trying to get a combination of builds that can approximate this (see this template) I still run into lots of bugs and compatibility issues. Many of these are because Parcel is trying to do something smart instead of straightforward.

I'd like to know:

  • Is there a best practice for doing this?
  • Is there a good way I can file actionable bugs for this?

I love Parcel because the development experience (especially compile times) is really great and "zero-config" used to work really well for me. I'd really like to figure out how to keep using it.

Bug ✨ Parcel 2

Most helpful comment

Anyone have any advice on this?

All 8 comments

Issues I currently haven't resolved include:

  • I have to build ≈4 targets and invoke Parcel separately for each build, which takes a lot longer.
  • #4731 Compatibility issues with node workers / comlink
  • Parcel 2 seems to assume each entry file corresponds to one target file, so I have to create a separate entry file for each target.
  • Using a separate types build seems result in dangling references in the generated d.ts files right now.
  • I run into JS errors at unpredictable times:

    • Issues with parcelRequire or require in browser builds, preventing code from running in the first place.

    • I seem to need to import runtime-regenerator/runtime sometimes, but I can't predict when. (Shouldn't the build be standalone?)

    • Builds that throw ______ is not a constructor for classes, which I can sometimes work around using a browserslist field in package.json but not always.

    • HMR breaks in one of three ways: responding with HTML for JS (unexpected <), appearing to recompile but using an old cached version of a file that I just changed, or just plainly failing to pick up on file changes.

Some things I've figured out:

  • Parcel uses the browser field of package.json in a dependency for all browser builds, so I can't name my "browser build" browser. (I've taken to calling it browser-global).

This should do what you want

{
    "type": "module",
    "main": "dist/index.js",
    "browser": "dist/browser.js",
    "types": "dist/index.d.ts",
    "targets": {
        "main": {
            "outputFormat": "esmodule"
        },
        "browser": {
            "includeNodeModules": true
        }
    }
}

(type: ”module" is rather new and not picked up by Parcel automatically)

puts exports on globalThis.

That is not implemented (yet).

a browser target that includes deps and puts exports on globalThis.

The problem I see here is that many bundlers prefer the browser entry to main, so your setup would not work well when bundled.

https://github.com/parcel-bundler/parcel/blob/cc5b4862699a9020c0931268e1c6e5b0437a6179/packages/resolvers/default/src/DefaultResolver.js#L18

This should do what you want

Thanks! I believe this still has a significant amount of issues for me. I'll try a branch of a project and identify exactly which issues remain.

I've tried taking a project and reducing the targets.

Build output for https://github.com/cubing/timer-db/commit/a6061a8d9aa61a43fad1b6a11e5adc58600f6bb2

✨ Built in 4.73s

dist/index.d.ts 1.61 KB 324ms
dist/browser-global.js 218.06 KB 365ms
dist/crypto.1a367996.js 795.25 KB 365ms
dist/index.js 11.71 KB 365ms
dist/crypto.1ca165c3.js 122 B 365ms
✨ Built in 3.16s

dist/browser-global.js 218.08 KB 392ms
dist/crypto.1a367996.js 795.25 KB 391ms

This still has the following issues:

  • The types target is still broken. [1]
  • Using npx parcel build tries to build all targets using the same source file (including the browser-global build that currently needs a separate source file). [2]
  • I can't require(".") in the node REPL. [3]
  • I can't import "./dist/index.js" from a module file in node without an error about an expected CommonJS format in one of the library's imports. [4]
  • I've already included a workaround for code splitting to work around a situation where the browser environment eagerly tries to import a require that is meant to be invoked conditionally (when running in node) that ideally I shouldn't have to.
  • I've already had to fiddle with tsconfig.json and .babelrc to work around other issues that happen out of the box.

[1] The generated dist/index.d.ts file references non-existent files:

import { Attempt, StoredAttempt, EventName } from "./data/Attempt";
import { StoredSessionMetadata, SessionMetadata } from "./data/SessionMetadata";
import { Storage } from "./storage/storage";
/******** SessionUUID ********/
type SessionUUID = string;

//... (expected export code)

export type { Attempt, StoredAttempt } from "./data/Attempt";

Each of those imported files don't exist in the build folder.

[2] I can try to fix this by replacing:

npx parcel build src/index.ts &&
  npx parcel build --target browser-global src/targets/browser-global.ts

with:

npx parcel build src/index.ts --target main &&
  npx parcel build src/index.ts --target types &&
  npx parcel build --target browser-global src/targets/browser-global.ts

That's already three invocations of Parcel. However, it doesn't work(!). The first two invocations actually both behave like npx parcel build src/index.ts.

Build output for https://github.com/cubing/timer-db/commit/71d8a0e6657d9a34046bcd4b5cec4c8651c6faf2

✨ Built in 1.28s

dist/index.d.ts 1.61 KB 327ms
dist/browser-global.js 218.06 KB 371ms
dist/crypto.1a367996.js 795.25 KB 371ms
dist/index.js 10.53 KB 372ms
dist/crypto.f937c408.js 120 B 372ms
dist/index.esm.js 11.71 KB 372ms
dist/crypto.1ca165c3.js 122 B 372ms
✨ Built in 1.56s

dist/index.d.ts 1.61 KB 414ms
dist/browser-global.js 218.06 KB 464ms
dist/crypto.1a367996.js 795.25 KB 464ms
dist/index.js 10.53 KB 463ms
dist/crypto.f937c408.js 120 B 463ms
dist/index.esm.js 11.71 KB 463ms
dist/crypto.1ca165c3.js 122 B 463ms
✨ Built in 1.34s

dist/browser-global.js 218.08 KB 389ms
dist/crypto.1a367996.js 795.25 KB 388ms

I can fix this by using a separate entry file for the types target:

npx parcel build --target main src/index.ts &&
  npx parcel build --target types src/targets/types.ts &&
  npx parcel build --target browser-global src/targets/browser-global.ts

Built output for https://github.com/cubing/timer-db/commit/630f34b717849cef517764409781bca28429a714

✨ Built in 2.97s

dist/index.js 11.71 KB 357ms
dist/crypto.1ca165c3.js 122 B 357ms
✨ Built in 3.22s

dist/index.d.ts 1.61 KB 14ms
✨ Built in 3.23s

dist/browser-global.js 218.08 KB 442ms
dist/crypto.1a367996.js 795.25 KB 441ms

[3] This is arguably not Parcel's fault. I can fix this by using four targets:

npx parcel build --target main src/index.ts &&
    npx parcel build --target module src/targets/module.ts &&
    npx parcel build --target types src/targets/types.ts &&
    npx parcel build --target browser-global src/targets/browser-global.ts

Build output for https://github.com/cubing/timer-db/commit/1c02556436d08b18932ed80650fcf8822c7cbd50

✨ Built in 3.35s

dist/index.js 10.53 KB 369ms
dist/crypto.f937c408.js 120 B 368ms
✨ Built in 2.86s

dist/index.esm.js 11.71 KB 363ms
dist/crypto.1ca165c3.js 122 B 363ms
✨ Built in 902ms

dist/index.d.ts 1.61 KB 6ms
✨ Built in 3.37s

dist/browser-global.js 218.08 KB 479ms
dist/crypto.1a367996.js 795.25 KB 478ms

[4] e.g. using node node-test.js in the commit in question. I still don't know how to resolve this.

At this point:

  • I have to use four invocations with 4 separate entry files.
  • The types output still doesn't work.

If I can fix the types issue, this isn't totally unreasonable. But this feels like a lot of workarounds to use a "zero-config" tool for a fairly straightforward project, and I'm worried this isn't the end of my issues. 😔

By the way, I also think that it's perfectly reasonable to say "Parcel 2 isn't meant to be used to maintain libraries in TypeScript" and e.g. say I should be using Rollup. I have a lot of qualms with Rollup and vastly prefer Parcel 2 (Parcel 1 was already excellent for building web apps!), but I can understand if Parcel 2 has different goals. I'm trying to get an understanding of the situation by filing this issue.

I'd also love to contribute fixes/improvements to Parcel 2, but at the moment I need to focus on the actual libraries I'm writing.

Parcel 1 wasn't meant to build libraries. But Parcel 2 is

Using npx parcel build tries to build all targets using the same source file (including the browser-global build that currently needs a separate source file)

Having entries assigned to specific targets isn't possible at the moment.

Using a single entry for main/module/targets should work, otherwise it's a bug.

For browser-global, you need to use a separate entry until we add back the --global option from Parcel 1.

There is a section about this in the docs, someone just needs to fill it with content. :slightly_smiling_face:

Anyone have any advice on this?

Was this page helpful?
0 / 5 - 0 ratings