Thursday, 15 July 2021

How to setup the TypeScript compiler for the library so that the unused modules will be cut off by Webpack in the dependents projects?

Preliminary explanations about subject library

I am sorry about taking your time by making you read this. I wrote it to answer the questions like "What are you doing?" and "Why are you doing this?".

The library consists from a large number of helper functions an classes. In this regard it's similar to lodash (check the structure of lodash), but unlike lodash the source code has been organized by multilevel directories. It's comfortable for the developers but could be not comfortable for the users: to import the desired function to project, the user must need to know where it is, e. g.:

import { 
  computeFirstItemNumberForSpecificPaginationPage
} from "@yamato-daiwa/es-extensions/Number/Pagination";

To solve this problem most of functions has been imported to index.ts and exported again from there. Now user can get desired function as:

import { 
  computeFirstItemNumberForSpecificPaginationPage 
} from "@yamato-daiwa/es-extensions";

Please note that all functions in index.ts (will be compiled by TypeScript to index.js) are intended for both BrowserJS and NodeJS. The functionality especially for BrowserJS is in BrowserJS.ts and especially for NodeJS in NodeJS.ts (currently is almost empty but the reexporting methodology is same).

Also, until this problem will be solved, I included the compiled JavaScript to the library repository (Distributable directory).

Problem

From now, @yamato-daiwa/es-extensions is the library and any project which depends on it is the consuming project.

I expected that all unused functions/classes of the consuming project will be cut off by Webpack optimizations. For example, in below case I expected that isUndefined function only will be left in Webpack bundle:

import { isUndefined } from "@yamato-daiwa/es-extensions"

const test: string | undefined = "ALPHA";
console.log(isUndefined(test));

But in reallity, Webpack left EVERYTHING from index.js of the library. I beautified the minified JavaScript built by Webpack; it is like:

(() => {
    "use strict";
    var e = {
            5272: (e, t) => {
                Object.defineProperty(t, "__esModule", {
                    value: !0
                }), t.default = function(e, t) {
                    for (const [a, n] of e.entries())
                        if (t(n)) return a;
                    return null
                }
            },
            7684: (e, t) => {
                Object.defineProperty(t, "__esModule", {
                    value: !0
                }), t.default = function(e, t) {
                    const a = [];
                    return e.forEach(((e, n) => {
                        t(e) && a.push(n)
                    })), a
                }
            },
  // ...

I suppose everyone understanding that is not acceptable especially for browser applications where each kilobyte on count.

How to solve this problem? The ideal solution (if it exists) will not touch the source files organization, just change the TypeScript configuration.

Repro

I created one more repository (repro) where you can try above example.

Experiment flow

  1. Get this repository by VCS
  2. Install dependencies as always (npm i command).
  3. Check the src/index.ts. It imports isUndefined function from the library and using it.
  4. Run npm run ProductionBuild
  5. Beautify the output index.js by tool like beautifier.io. You will see that whole library has been bundled while it's desired that only inUndefined has been bundled.

Musings about the cause

The first cause candidate is the using of reexportint pattern, exactly Source/index.ts, Source/BrowserJS.ts and Source/NodeJS. The compiled index.js looks like:

const isStringifiedNonNegativeIntegerOfRegularNotation_1 = require("./Numbers/isStringifiedNonNegativeIntegerOfRegularNotation");
exports.isStringifiedNonNegativeIntegerOfRegularNotation = isStringifiedNonNegativeIntegerOfRegularNotation_1.default;
const separateEach3DigitsGroupWithComma_1 = require("./Numbers/separateEach3DigitsGroupWithComma");
exports.separateEach3DigitsGroupWithComma = separateEach3DigitsGroupWithComma_1.default;

(Check full file)

If to import each function from it's individual module like import isUndefined from "@yamato-daiwa/es-extensions/TypeGuards/isUndefined" instead of import { isUndefined } from "@yamato-daiwa/es-extensions", no redundant code will be output. But as I already told, this solution is unacceptable because the library users must need to know where isUndefined and other function has been organized.

The other cause could be the output modules type. Currently it's a CommonJS. Here is the tsconfig.json of the library:

{
  "compilerOptions": {

    "target": "ES2020",
    "module": "CommonJS",
    "moduleResolution": "Node",

    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,

    "removeComments": true,

    "outDir": "Distributable/",
    "declaration": true
  },

  "include": [ "Source/**/*" ]
}

According to hypothesis, depending on specific modules type the Webpack could bundle the code to monolithic structure where it's impossible to decompose and filter out some modules even if those has not been used.

Now all these (AMD, UMD, CommonJS) slowly become a part of history, but we still can find them in old scripts.

🌎 javascript.info

By the way, the TypeScript configuration in consuming project also could affect (included to repro). Currently it is:

{
  "compilerOptions": {

    "target": "ES2020",
    "strict": true,

    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "skipLibCheck": true,

    "baseUrl": "./",
    "paths": {
      "@SourceFilesRoot/*": ["./src/*"]
    }
  }
}


from How to setup the TypeScript compiler for the library so that the unused modules will be cut off by Webpack in the dependents projects?

No comments:

Post a Comment