Parcel: babel transpile node_module dependencies

Created on 3 Jul 2018  ยท  17Comments  ยท  Source: parcel-bundler/parcel

โ” Question


I need a way to babel transpile some dependencies in node_modules, and there doesn't seem to be a way to do so with parcel.

I've read these relevant issues: #948 #13 , and I'm aware of #1101 which solves the case for local repo symlink.

I'm using file: dependencies to reference local repos in my project and I need to run babel for those, but no existing solution covers this case.

What would the problems be if we simply specify the target node_modules that need to be transpiled with babel? I'm curious why this wasn't considered as an alternative solution to #1101

Having the repos flag themselves as "source" only solves part of the problem, and being able to manually specify packages from the root project seems a lot more intuitive and flexible. I agree that the community needs to collectively come up with a cleaner, more thought-out solution for managing code compatibility for released packages/modules, but until then I believe it is up to the package consumers to make sure that every code they import and release into production is transpiled and made compatible with their desired browser targets. Parcel is a tool for package consumers, therefore I feel it should support such use case.

๐ŸŒ Your Environment

| Software | Version(s) |
| ---------------- | ---------- |
| Parcel |1.9.3
| Node |8.9.1
| npm/Yarn |5.5.1/1.3.2
| Operating System | MacOS HighSierra 10.13.3

Feature babel โœจ Parcel 2

Most helpful comment

I have a feeling people in this thread are talking of two different things as if they were the one and same...

.browserslistrc is meant to specify what browsers the transpiled code is supposed to be compatible with. Alternatively you could put that in the package.json.

But the problem discussed in this thread is that Parcel (v 1.*) doesn't transpile external node modules even if you put a browsers list in your project. The root cause of this is that Parcel assume NPM packages are ES5 by default and thus no need to transpile them.

One then needs to find some way to "convince" Parcel somehow that the NPM package is actually in a very modern form of JavaScript and needs to be transpiled to be browser-compatible. One hacky way of doing this is putting a .browserslistrc in the module folder (not the root folder) to make Parcel "understand" that the module actually has a very modern form of JS. That is the purpose of the .browserslistrc.package discussed above by @icopp.

So, while the root .browserslistrc should correctly be conservative in what compatibility level they prescribe in order to make sure the transpiled code is compatible in common browsers - the .browserslistrc that is copied into the NPM modules should actually prescribe a very 'modern' form of JavaScript (e.g. node 10.11), because they're not used to describe the _output_, but the _input_ for transpilation.

All 17 comments

This seems like a relevant place to mention that the local modules (src/node_modules) pattern does not work in Parcel, due to the above restriction; Babel does not seem to transpile dependencies in any node_modules, including src/node_modules.

It should also be noted that in order to _effectively_ use the local modules pattern, the --no-autoinstall option must be used, to avoid installing npm modules instead of local modules.

Right now a project I'm currently working on is using along the lines of:

// .browserslistrc.packages
node 10.11

// package.json
{
  "scripts": {
    "postinstall": "npm-run-all -p \"postinstall:*\"",
    "postinstall:p-retry": "cpy --rename=.browserslistrc .browserslistrc.packages node_modules/p-retry",
    "postinstall:query-string": "cpy --rename=.browserslistrc .browserslistrc.packages node_modules/query-string"
  }
}

It's super hacky, but it more or less works.

Babel 7 allows custom transpilations of dependencies in node_modules with project-wide babel.config.js and overrides property (Link).

module.exports = {
  ...
  overrides: [
    {
      test: ["./node_modules/myPackage/dist"],
      plugins: ["@babel/plugin-transform-modules-commonjs"]
    }
  ]
}

Can we somehow emulate this on Parcel 1.x ? Will be such scenarios supported in Parcel 2, where a package consumer can decide how he transpiles a package?

E.g. a use case would be a common module with es-next language syntax which can be consumed by browser (consider transpile to browser target, use babel plugins like @babel/plugin-proposal-class-properties) or in an Electron environment without any conversion, as a very decent chromium browser is used.

I know, there is source field - in this case though it would not be viable.

Will be such scenarios supported in Parcel 2, where a package consumer can decide how he transpiles a package?

Parcel 2 will use the provided babel config without modification (and will most likely also support babel.config.js).


For future reference, from the other issue:

For example, I'd like to use the only or include fields of my .babelrc to specify that it should be used to transform specific files.

Right now a project I'm currently working on is using along the lines of:

// .browserslistrc.packages
node 10.11

// package.json
{
  "scripts": {
    "postinstall": "npm-run-all -p \"postinstall:*\"",
    "postinstall:p-retry": "cpy --rename=.browserslistrc .browserslistrc.packages node_modules/p-retry",
    "postinstall:query-string": "cpy --rename=.browserslistrc .browserslistrc.packages node_modules/query-string"
  }
}

It's super hacky, but it more or less works.

Are there other requirements for that to work? Every IE support issue in this tracker is being closed and marked as duplicate of this one. But I cannot get IE 11 to work.

Even adding a .browserslistrc to every node_module defined in my package.json, does not solve the trouble. I still end up with arrow functions in my bundle.

What am I missing?

const fs = require('fs');

const package = JSON.parse(
  fs.readFileSync('package.json', { encoding: 'utf8' })
);

const modules = Object.keys(package.dependencies);
const browserslistrc = '.browserslistrc';

modules.forEach(name => {
  fs.copyFileSync('.browserslistrc', `node_modules/${name}/${browserslistrc}`);
  console.log(`copied ${browserslistrc} to node_modules/${name}`);
});

If I first run that script, and after it rm -rf dist\ .cache\ && npx parcel index.html, I still end up with the arrow functions in the bundle. But also, the build still completes in 115s. Giving the impression that not every module is being transpiled.

node 10.11 will still emit arrow functions IIUC, you probably need a more robust browserslist config than that to get it to transpile to something that IE 11 will understand

@floyd-may , I've been defining IE 11. This is my .browserslistrc. Is the trick here to use node? This should work also, right?

last 2 chrome versions
last 2 firefox versions
last 2 edge versions
last 2 opera versions
last 2 safari versions
IE 11

Does the fact that I'm using both .ts as well as .js files have anything to do with this?

@smeijer Your .browserslistrc appears correct (I think you can check exactly which browsers it's compiling for with npx browserslist).

Part of the issue is that Parcel seems to strictly require you to use npm packages that follow the general convention that bundled/compiled versions of a package are published as ES5 code for maximal compatibility. The downside is that when (for whatever reason) you need to use a package that does not follow this convention, there doesn't seem to be an easy way to make Parcel transpile the module.

The hack I figured out when I used to use Parcel was to copy whatever ES6 file(s) were causing grief and manually transpile them. Then I just used the transpiled versions and ignored the original module. The disadvantage of this approach is that it makes receiving updates hard, but chances are the modules which don't bother transpiling down to ES5 probably aren't updated frequently anyways.

The long-term solution I found was to switch to Webpack 4 for new projects. It incrementally rebuilds much faster (at least for me) and it provides much greater flexibility over how Babel and other processes run.

I have a feeling people in this thread are talking of two different things as if they were the one and same...

.browserslistrc is meant to specify what browsers the transpiled code is supposed to be compatible with. Alternatively you could put that in the package.json.

But the problem discussed in this thread is that Parcel (v 1.*) doesn't transpile external node modules even if you put a browsers list in your project. The root cause of this is that Parcel assume NPM packages are ES5 by default and thus no need to transpile them.

One then needs to find some way to "convince" Parcel somehow that the NPM package is actually in a very modern form of JavaScript and needs to be transpiled to be browser-compatible. One hacky way of doing this is putting a .browserslistrc in the module folder (not the root folder) to make Parcel "understand" that the module actually has a very modern form of JS. That is the purpose of the .browserslistrc.package discussed above by @icopp.

So, while the root .browserslistrc should correctly be conservative in what compatibility level they prescribe in order to make sure the transpiled code is compatible in common browsers - the .browserslistrc that is copied into the NPM modules should actually prescribe a very 'modern' form of JavaScript (e.g. node 10.11), because they're not used to describe the _output_, but the _input_ for transpilation.

@arntj thanks for the clarification!

I have a feeling people in this thread are talking of two different things as if they were the one and same...

.browserslistrc is meant to specify what browsers the transpiled code is supposed to be compatible with. Alternatively you could put that in the package.json.

But the problem discussed in this thread is that Parcel (v 1.*) doesn't transpile external node modules even if you put a browsers list in your project. The root cause of this is that Parcel assume NPM packages are ES5 by default and thus no need to transpile them.

One then needs to find some way to "convince" Parcel somehow that the NPM package is actually in a very modern form of JavaScript and needs to be transpiled to be browser-compatible. One hacky way of doing this is putting a .browserslistrc in the module folder (not the root folder) to make Parcel "understand" that the module actually has a very modern form of JS. That is the purpose of the .browserslistrc.package discussed above by @icopp.

So, while the root .browserslistrc should correctly be conservative in what compatibility level they prescribe in order to make sure the transpiled code is compatible in common browsers - the .browserslistrc that is copied into the NPM modules should actually prescribe a very 'modern' form of JavaScript (e.g. node 10.11), because they're not used to describe the _output_, but the _input_ for transpilation.

Exactly! I don't want to assume that all node_modules are already transpiled. Not every developer follows the same browser support so assuming node_modules are safe for every project feels irresponsible.

So if I'm understanding correctly; I have been doing this wrong? https://github.com/parcel-bundler/parcel/issues/1655#issuecomment-549442120

I've been copying .browserslistrc into the modules with IE 11 defined, because I need to support IE11. But what I was doing instead, was telling parcel that this module already was in IE 11 compatible syntax?

So, .browserslistrc in node_modules tell's parcel which browsers are already supported. Not into which syntax it should be compiled. And .browserlistsrc in your own project tell's parcel which browsers should be supported and be compiled for.

So
./node_modules/x/.browserslistrc defines input syntax
./.browserslistrc defines output syntax.

correct?

I used parcel and svelte for several months,
during which I developed a more accurate postinstall script to solve this issue.

// postinstall.js
const fs = require('fs')
const nodeVersion = 'node 10.11'

// Patch to node_modules/*
const patch = (staticPath) => {
    let folderNames = fs.readdirSync(staticPath)
    for(let folderName of folderNames){
        let stats = fs.statSync(staticPath + '/' + folderName)
        if(! stats.isDirectory()) continue

        try{
            let packageFilePath = `${staticPath}/${folderName}/package.json`
            let browserListFilePath = `${staticPath}/${folderName}/.browserslistrc`
            let packageFileData = JSON.parse(fs.readFileSync(packageFilePath))

            delete packageFileData['browserslist']
            fs.writeFileSync(browserListFilePath, nodeVersion)
            fs.writeFileSync(packageFilePath, JSON.stringify(packageFileData, null, 2))
            // console.log(`Fixed browserlist in ${packageFilePath}`)

            // Patch to node_modules/*/node_modules/*
            let nestedModulePath = `${staticPath}/${folderName}/node_modules`
            if(fs.existsSync(nestedModulePath)) patch(nestedModulePath)
        }catch(e) {}
    }
}

patch('./node_modules')
console.log(`All browserlist has been updated.`)

Enter the above code into the project main folder to 'postinstall.js'
and add the following script to package.json.

{
  "scripts": {
    "postinstall": "rm -rf ./.cache && node ./postinstall.ts"
  }
}

Now, if you do npm install, the browser list is automatically patched at the end.

CAUTION

Postinstall does not appear to run automatically on manual module installation commands such as npm install <module name>, The npm run postinstall command must be manually executed after you install the manual module install.

@hmmhmmhm

CAUTION

Postinstall does not appear to run automatically on manual module installation commands such as npm install <module name>, The npm run postinstall command must be manually executed after you install the manual module install.

as npm hooks mentioned, a not so elegant way to run Postinstall automatically is add node_modules/.hooks/postinstall :

npm run postinstall

remember to run chmod +x node_modules/.hooks/postinstall, otherwise it maybe permission denied

Does anyone know if the latest version of Parcel still has the same limitation?

I'm asking because @arntj specifically mentioned Parcel v1.* in his comment, so I'm wondering if the new version has a solution to this problem.

But the problem discussed in this thread is that Parcel (v 1.*) doesn't transpile external node modules even if you put a browsers list in your project. The root cause of this is that Parcel assume NPM packages are ES5 by default and thus no need to transpile them.

It's been more than a year and I still think it's not practical to have the tool rely entirely on the community upholding a certain standard.

๐Ÿ˜”

When you place a .browserslistrc in a node_modules external package directory with the JS dialect it's written in (eg node 10), then parcel is transpiling the external package, which is pretty nice (and still hacky, but easily done with some scripting, see examples above).

However, when a "browserslist" section already exists in the module package.json this will get presedence so the .browserslistrc will be ignored. It took me hours to figure this out so maybe I'll prevent some frustration with others by posting this.

(The script above by hmmhmmhm is actually removing the existing browserslist key from a package.json file)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dsky1990 picture dsky1990  ยท  3Comments

mnn picture mnn  ยท  3Comments

davidnagli picture davidnagli  ยท  3Comments

will-stone picture will-stone  ยท  3Comments

oliger picture oliger  ยท  3Comments