Ignore or replace dependencies' imports when bundling with Rollup - javascript

I'm bundling a JS library using Rollup. This lib has a dependency on #tensorflow/tfjs-core.
On tfjs's code, there's a function that fetches a URL. If it's in the browser environment, it uses the global fetch function; if it's not, it tries to import node-fetch.
Something among these lines:
fetch(path: string, requestInits?: RequestInit): Promise<Response> {
if (env().global.fetch != null) {
return env().global.fetch(path, requestInits);
}
if (systemFetch == null) {
systemFetch = require('node-fetch');
}
return systemFetch(path, requestInits);
}
My library is made to run in the browser, so it always uses the global fetch function. However, Rollup still bundles node-fetch's require in my lib's assets.
It should not be an issue, but some consumers are reporting errors when using the library in a React project that uses webpack:
Failed to compile.
./node_modules/[my lib]/index.js
Cannot find module: 'node-fetch'. Make sure this package is installed.
You can install this package by running: npm install node-fetch.
Question is: is there some way I can tell Rollup not no bundle this?
I thought about replacing the require('node-fetch') by undefined after the bundle is generated, but it feels like a dirty hack. Any other sugestions?
PS: I believe marking node-fetch as external on consumer projects would fix the issue, but since I do not use node-fetch in my lib, it would be nice to remove it from final output.

Other package managers can include or exclude files based on the environment, test, development, production, etc.
There is any number of ways of implementing this, even going so far as
# Makefile
ENVIRONMENT ?= test
ROLLUP = $(which rollup)
ENVSUBST = $(which envsubst)
rollup.config.js: src/$(ENVIRONMENT)
${ENVSUBST} < $# > $^
${ROLLUP} $^ -o $(ENVIRONMENT).js
If you created files named after your environments, you could compile them using
make -e environment=browser
I don't expect my code to work, only to express ideas.

There is this loc which is used to exclude node-fetch from the bundle. You could consider a similar approach in your rollup configuration. (I think) If you add that, node-fetch will/should not be a part of your minified library.

Related

How to compile/build a TypeScript library that uses no NodeJS API/Module dependency to support browser

I wrote a few code that use plain browser Javascript APIs only and can be run well within browser HTML (served by IIS Server or Chrome Extensions). Now I want to contribute to the community by writing a library I have not seen on the market yet. However looking at current solutions, I am at loss at how a project is even built (WebPack/Browserify etc). A side note: I never actually work with NodeJS/NPM before.
For example I have this TypeScript project with the main file AwesomeClass.ts like this:
import { Helper1 } from "./Helper1.js";
import { Helper2 } from "./Helper2.js";
export class AwesomeClass {
doSomething() {
new Helper1().doSomething();
new Helper2().doSomething();
}
}
When built with tsc (I use VS Code as IDE), I can perfectly put this inside an Javascript module and browser can run it.
import { AwesomeClass } from "./AwesomeClass.js";
// Do something with AwesomeClass
So my question is, how do I build and distribute AwesomeClass? Maybe no NPM needed, but from a CDN? Ideally, I think somehow I should have the following output in a dist folder and developer can refer them either by hosting the files by themselves or use a CDN:
awesomeclass.js: For those who want to just use AwesomeClass without module feature (I think it's called UMD?). I.e. expose the AwesomeClass to global scope.
awesomeclass.es6.js: For those who want to use AwesomeClass by using import statement, like import { AwesomeClass } from "https://cdn.example.com/awesomeclass.es6.js";. I like this approach best and want to use this.
I should have something like awesomeclass.d.ts so those using TypeScript can use it. This one is especially tricky because so far I still don't understand how to make it work for 2nd scenario. TypeScript cannot get the type from an import statement from Javascript, and even ignoring that, I cannot get any typing for import statements.
In all cases, I would rather have only one js/ts file packed together if possible but not a deal breaker if I cannot (i.e. user will have to download Helper1.js and Helper2.js as well if I cannot).
Here's my current tsconfig.json:
{
"compileOnSave": true,
"compilerOptions": {
"noImplicitAny": true,
"target": "ES2020",
"module": "ES2020",
"declaration": true
},
"exclude": [
"node_modules"
]
}
There's quite a lot of things you're asking and each of them have several answers so I'll try to provide you with a bit of an overview of your options.
Compiling everything to a single file
One of the things you asked is how you can compile everything to a single file. You can do that in 2 different ways, either using webpack to bundle it for client or using typescript directly.
If you use typescript you have to set outFile to a specific file, in which case it will compile everything to that file, however you can only do that if your module is also set to amd or system, both of which are not ideal. While this works it's something I'd suggest you don't use.
Instead you should use webpack to bundle all your stuff with the output option, in which case webpack will use ts-loader to invoke typescript for you, compile your stuff and bundle it into a single file.
You should also note here that this is only applicable if you actually want to serve it through web and not if you're building a library. If you're building an npm package that you're planning on letting people install with something like npx packageName so that you can use it like import somePackage from some-package, then you should be compiling your stuff to a /lib directory into normal javascript and just let them import it as javascript. There's no reason for why you should provide them with the original typescript in that case.
How to build and distribute it
It really depends on what exactly you're building and how it'll be used, however overall you have 2 main options.
You can either host it somewhere on some server with a domain of your choice so that people can download it. Or you can put it anywhere like a normal git repo where people can download it. In this case you'll have to compile it with webpack yourself, upload it yourself and then just share the link with people i.e. https://example.com/downloads/awesome. Alternatively you can use webpack to render it server side and expose an API to people that they can call in order to get your code, then it will deliver the bundled javascript to them once they call the API i.e. https://api.example.com/awesome which will hit your API with a GET request, which will route to awesome and then you invoke webpack's compiler to bundle your code server side.
Your other option is to build your package like normal, compile it and then use the official npm registry to host your npm package. Using this option will allow people to npx package or npm i package on your code and also allow them to use it like import awesome from 'awesome'. If you go this route then using webpack isn't necessary, or it depends, because people using it will import it into their own project and build it into their own webpack setup and bundle if required. In this case all you have to do is compile your typescript to something like a /lib and allow them to install and import it.
From the things that you're asking/saying it seems to me that you're trying to create an npm package, for that all you need is to create the package, compile your typescript, set up an account on npm and push your package to their registry, from where you can let anyone install it. For this you also shouldn't care at all about compiling all your code to a single file because it doesn't matter, if they use import awesome from 'awesome' then that file can again import anything else inside your own package and they wouldn't know it. You can just tsc your code to an output directory and let them know which is the default export for that package.
If your code has to run in browser then I don't believe just using typescript will be enough, in that case you'll have to use webpack, you might also need babel if you need to support older browsers and polyfills, which is something you can add to webpack, then you'll use webpack to compile your bundle. Webpack will then invoke typescript, through ts-loader, which will compile and bundle your code for you ready for web. In this case you'll still need to push this code to the npm registry as a package so others can use it.
The choice between those options is entirely dependent on what it is and who's going to use it and how.
This should be the core requirements in the tscongfig.json file
{
"compilerOptions": {
"lib": ["es6", "es2020.promise", "dom", "es2020"],
"declaration": true,
"target": "es2020",
"module": "es2020"
}
}
While to pack everything in just one file, I think you have to use something like webpack; but I don't know enough this topic to help you on that, sorry.

Include JS module/file only in development-mode

How can I conditionally import a module only in development mode (in my case the axios-mock-adapter package). Also the code should not even be present in the production bundle.
Example code I only want to be included during development:
export const mockUpClient = (api: AxiosInstance): void => {
// full api mocking, containing lots and lots of data
}
Now I am importing the module based on the following condition:
if (process.env.NODE_ENV === 'development') {
import("./apiMockAdapter").then((module) => {
module.mockUpClient(api)
})
}
The code is still included in the build, however it is not executed in production mode. How is it possible to completely exlude the code from the production bundle (of course without commenting out the code before every build)?
Update
The above example works fine. Before asking the question, I also imported the file from somewhere else, which led to this behaviour.
The accepted answer explains in detail how webpack will bundle the code & modules.
Basically:
Eject from create-react-app with npm run eject. You may be worried about the maintenance burden but it you look at the create-react-app repo you'll see there are very few meaningful changes in CRA and the upkeep with it is actually higher. If you are insistent on CRA then use craco.
Go to webpack.config.js (or craco.config.js if using craco)
Add an externals field if the app is running in production mode
Should look something like this. In this object add an externals part:
externals: isEnvProduction ? {
'myApiAdapter' : 'window' // or something else global
} : undefined,
This will map import('myApiAdapter') to window in production builds and not include it in the bundle.
That said, webpack should see the dynamic import as a point to break the bundle down into chunks, so it's unclear without seeing your actual code why it is included. Making that file external should bypass any such issues.

Setting environment configuration in a TypeScript app with Webpack

I have gone through several solutions including the ones listed here:
Environment Variables in an isomorphic JS app: Webpack find & replace?
Passing environment-dependent variables in webpack
I am used to using something like gulp-replace-task to find and update a config file for the app to replace things like ##SERVER_URL with something set from the environment.
That way I can do export SERVER_URL=something or run the script with SERVER_URL=something gulp build to set the configuration.
I've tried all of the following:
Using the transform-loader plus envify
This is a suggestion from the first question, but it does not work for me because of:
Module build failed: Error: Parse Error: Line 1: Illegal import declaration
at throwError (ngapp/node_modules/esprima-fb/esprima.js:2823:21)
Seems like esprima-fb is using an import declaration that Webpack can't use for some reason or another. The project is no longer maintained either, so this may be the wrong road to go down.
Using DefinePlugin
I've added:
module: {plugins: [new webpack.DefinePlugin({"process.env.SERVER_URL": "something"})]}
This seems to be ignored, or at least process.env.SERVER_URL does not get interpolated in my typescript files. When I console.log(process.env), it emits an empty object.
Setting using --define for webpack
I updated my npm script:
"start": "webpack-dev-server --define process.env.SERVER_URL=${SERVER_URL}"
However this just ends up replacing process.env.SERVER_URL in my code with a literal "${SERVER_URL}" rather than being interpolated in the npm script.
Is there a simple / convenient (or at this point really any) way to use environment variables in TypeScript apps built with Webpack?
My Webpack setup is essentially what is listed in the Angular docs.

Webpack importing video.js returns an empty object

I am trying to use video.js via webpack.
I installed video.js via npm - npm install video.js --save-dev
In webpack I read that video.js should be loaded via script loader else it throws an error.
This is how I am loading video.js through the babel loader
module:
loaders: [
{
test: /video\.js/,
loader: 'script'
}
]
I got this solution from here https://github.com/videojs/video.js/issues/2750
This is my import statement
import videojs from 'video.js';
The issue that I now face is the import is returning an empty object, so when I try to do this:
var vidTag = ReactDOM.findDOMNode(this.refs.html5Video);
this.videojs = videojs(vidTag);
I get this error:
renderer-0.js:8031 Uncaught (in promise) TypeError: (0 , _video2.default) is not a function(…)
Any help will be much appreciated. I am new to ES6 / React / Webpack
Please take a look at the loader's README before copy&pasting some random code. The script-loader is not appropiate here, because it imports scripts into the global scope while skipping the whole module system.
So, if you wanted to use the script-loader, you would just write:
import "script-loader!video.js";
console.log(videojs); // should be an object now
Usually I would not recommend the use of the script-loader because it neglects the whole point of a module system where you import stuff explicitly into the local scope. In the example above, the import happens as a side-effect into the global scope which is effectively the same as just using a <script> tag with all its downsides like name clashes, etc.
There are often better alternatives to it, like the exports-loader, which appends a module.exports at the end of the module, thus turning an old-school global script into a CommonJS module.
In this particular case, however, you don't need a loader at all because video.js is already aware of a CommonJS module system. Just write import videojs from "video.js";.
There is another minor problem, however. If you compile this with webpack, it will print a warning to the console:
WARNING in ../~/video.js/dist/video.js
Critical dependencies:
13:480-487 This seems to be a pre-built javascript file. Though this is possible, it's not recommended. Try to require the original source to get better results.
# ../~/video.js/dist/video.js 13:480-487
This is because webpack detects that this file has already been bundled somehow. Often it's better to include the actual src with all its tiny modules instead of one large dist because this way webpack is able to optimize the bundle in a better way. I've written down an exhaustive explanation about how to import legacy scripts with webpack.
Unfortunately, video.js does not include its src in the version deployed at npm, so you're forced to use the dist. In order to get rid of the error message and to improve webpack's build time, you can instruct webpack to skip video.js when parsing the code for require() statements by setting the module.noParse option in your webpack.config.js:
module: {
noParse: [
/node_modules[\\/]video\.js/
]
}
Usually it's safe to flag all pre-bundled modules (typically those with a dist folder) as noParse because they are already self-contained.
include SDN
<script src="//vjs.zencdn.net/5.11/video.min.js"></script>
webpack config:
config.externals = {
'video.js': 'videojs'
};

How does require('atom') work?

Atom exposes some global APIs that you can access from require('atom')
How does this functionally work? Atom packages don't explicitly have atom as a dependency, yet they can still do this. Moreover, how can I do this in my own Electron application with my own global package?
I've gone through and analyzed Atom's source myself to determine how this happens, and this is what I've come up with.
Atom packages are required using the normal node require. However, according to the apm readme:
The other major difference is that Atom packages are installed to
~/.atom/packages instead of a local node_modules folder...
So the require('atom') package isn't retrieved from a parent node_modules directory like normal node modules. Instead, Atom overrides the module loader to change the behavior a bit.
More specifically, they override Module._resolveFilename like so:
Module = require 'module'
Module._resolveFilename = (relativePath, parentModule) ->
resolvedPath = resolveModulePath(relativePath, parentModule)
resolvedPath ?= resolveFilePath(relativePath, parentModule)
resolvedPath ? originalResolveFilename(relativePath, parentModule)
It attempts to resolve the path of a module with its own module cache logic before defaulting to normal behavior. This is done for a couple reasons that I can tell.
It lets them hardcode the path of builtin modules like 'atom', even though the normal behavior would never have found it.
It prevents loading package dependencies twice when packages have the same dependency with compatible versions. If packageA loads lodash#4.x.x and later packageB attempts to load lodash#>=3, then Atom steps in and gives packageB the lodash that packageA loaded.

Categories