My JavaScript Build Tool Setup - egnomerator/misc GitHub Wiki

My JavaScript Build Tool Setup in Detail

This document is meant to explain specific configurations I use.

  • when i started working on this, i was using the latest LTS version of Node.js - v14.17.5 (with NPM v6.14.14)

Check this other document for a high-level explanation of the purpose of these tools:

note on JavaScript build output:

  • the result of my JavaScript build process is a couple of output files in a dist folder
    • i set these output files to always be copied to the MSBuild output directory
    • these output files should be deployed
  • all the below config files work together to produce the output files
    • i set all the config files to never be copied to the MSBuild output directory
    • these config files should not be deployed

babel.config.js

module.exports = {
    presets: [
        // determines plugins you need for you
        // - based on the JS features used and build target
        // https://babeljs.io/docs/en/babel-preset-env
        ["@babel/preset-env"],
        // https://babeljs.io/docs/en/babel-preset-react
        ["@babel/preset-react"],
        // https://babeljs.io/docs/en/babel-preset-typescript
        ["@babel/preset-typescript"]
    ]
}

Separate file

  • with a separate babel config file, Jest and Webpack can access that common configuration
  • this gets consistent transpilation for the web app and for the unit tests

package.json

https://docs.npmjs.com/cli/v6/configuring-npm/package-json

not publishing

  • the package.json file can have A LOT of properties focused on a package intended for publishing
  • i'm not trying to publish an NPM package
  • i'm just trying to use it to define my app's build process and dependencies
{
  "version": "1.0.0",
  "name": "aspnetcoremvc-jquery-to-react",
  "private": true,
  // the following scripts are designed to provide a consistent build process
  // - check for any TypeScript issues--if any stop build
  //    - tscwatch does this but in watch mode
  // - then build for prod, dev, or test
  "scripts": {
    // build the output for production (calls the typescheck script)
    "prod": "npm run typescheck && webpack --mode=production",
    // build the output for dev--debugging (calls the typescheck script)
    "dev": "npm run typescheck && webpack --mode=development",
    // build the output for unit testing (calls the typescheck script)
    "test": "npm run typescheck && jest",
    // dev in watch mode (calls the tscwatch script)
    "devw": "npm run tscwatch -- --onSuccess \"webpack --mode=development\"",
    // test in watch mode (calls the tscwatch script)
    "testw": "npm run tscwatch -- --onSuccess jest",
    // run get a unit test coverage report based on "jest" config section below
    "testc": "npm run typescheck && jest --coverage",
    // perform TypeScript type-checking in watch mode
    "tscwatch": "tsc-watch -p ./tsconfig.bld.json",
    // perform TypeScript type-checking
    "typescheck": "tsc -p ./tsconfig.bld.json",
    // generate declaration files
    "typesemit": "tsc -p ./tsconfig.dts.json",
    // view resolved configuration results that tsc would use given the tsconfig file
    "tsconfide": "tsc -p ./tsconfig.json --showConfig",
    "tsconfbld": "tsc -p ./tsconfig.bld.json --showConfig",
    "tsconfdts": "tsc -p ./tsconfig.dts.json --showConfig"
  },
  // specify build targets
  // https://babeljs.io/docs/en/babel-preset-env#browserslist-integration
  "browserslist": [
    "defaults"
  ],
  "jest": {
    // determine what files under these paths are covered by unit tests
    "collectCoverageFrom": [ "wwwroot/app/src/**/*", "!**/__snapshots__/**" ],
    // save coverage reports here
    "coverageDirectory": "<rootDir>/node_modules/_jest-coverage-reports"
  },
  // keep prod dependencies to an absolute minimum
  // - i like to install an exact version and use the --save-exact flag
  "dependencies": {
    "react": "17.0.2",
    "react-dom": "17.0.2"
  },
  // minimize use of dev dependencies too
  // - i like to install an exact version and use the --save-exact flag
  "devDependencies": {
    // some babel packages for transpilation
    "@babel/core": "7.15.0",
    "@babel/preset-env": "7.15.0",
    "@babel/preset-react": "7.14.5",
    "@babel/preset-typescript": "7.14.5",
    // TypeScript types for intellisense
    "@types/jest": "27.0.2",
    "@types/jquery": "3.5.8",
    "@types/react": "17.0.2",
    "@types/react-dom": "17.0.2",
    "@types/react-test-renderer": "17.0.1",
    // Jest can use babel
    "babel-jest": "27.0.2",
    "babel-loader": "8.2.2",
    "jest": "27.0.2",
    // react-test-renderer is a helpful snapshot testing library
    "react-test-renderer": "17.0.2",
    "tsc-watch": "4.4.0",
    "typescript": "4.4.3",
    "webpack": "5.51.1",
    "webpack-cli": "4.8.0",
    "webpack-strip-block": "0.3.0"
  }
}

package-lock.json

lock files are big - example: https://github.com/egnomerator/LibReactComponentsStarter/blob/main/LibReactComponentsStarter/package-lock.json

  • i like using the --save-exact flag when installing dependencies
  • and i like using npm ci to refresh packages based on this file

tsconfig.json

{
  "compilerOptions": {
    // do not generate any files, i only want you to check for TypeScript errors
    "noEmit": true,
    // i want flexibility--so allow JS files to be imported
    "allowJs": true,
    // i want flexibility--so don't check and throw errors for my regular JS files
    "checkJs": false,
    // i'm using babel for transpilation, so warn me about this issues
    // https://www.typescriptlang.org/tsconfig#isolatedModules
    "isolatedModules": true,
    // this property is required to use JSX in TypeScript files
    "jsx": "react",
    // i want flexibility--so don't check and throw errors for this
    // - maybe set to true in the future, if i get more comfortable with TypeScript
    "noImplicitAny": false,
    // ES2015 (a.k.a. ES6) is the module syntax i'm using
    "module": "ES2015",
    // https://www.typescriptlang.org/docs/handbook/module-resolution.html
    "moduleResolution": "Node"
  },
  // WITHIN THE INCLUDE, do not process any files under these paths
  "exclude": [ "wwwroot/app/dist/**/*", "bin/**/*" ]
}

This main tsconfig file is for my intellisense

telling TypeScript intellisense to ignore the build output

  • "include" defaults to everything (**)
  • "exclude"
    • i'm excluding the dist folder, because i don't want to see intellisense messages/warnings about contents in my dist folder which might trigger linting warnings
    • same exclusion reason for the bin folder

moduleResolution

  • i wanted the "Node" resolution so that i could use this convention:
    • MyReactComponent/index.jsx
    • my folder is my component name, my index file is the component
    • and i can import the component in another module with import { MyReactComponent } from "./MyReactComponent"
    • Node module resolution will look for the index file under that folder
  • this lets me
    • organize my folder structure to have files pertaining to my component in the same folder
      • e.g.
        • /MyReactComponent
        • /MyReactComponent/__snapshots__
        • /MyReactComponent/index.jsx
        • /MyReactComponent/index.test.jsx
        • /MyReactComponent/props.jsx
        • /MyReactComponent/state.jsx
    • i also had a ton of errors about not being able to resolve NPM packages (modules in the node_modules folder)
      • these errors suggested using Node moduleResolution
      • so i followed this suggestion, and that fixed all the errors

tsconfig.bld.json

{
  // "extends"
  // use all the settings in tsconfig.json
  // - but override any of those settings with what's in here
  // - and use any additional settings in here
  "extends": "./tsconfig.json",
  // only process the folders/files under "wwwroot/app/src"
  "include": [ "wwwroot/app/src/**/*" ]
}

This tsconfig.bld file is for my actual build process

  • there are almost no changes from my intellisense-focused tsconfig file
  • that's because i want my intellisense to be accurate

The main goal i had was intellisense across ALL my JavaScript files, and build ONLY my JavaScript modules (ES6 modules)

  • I want my build process to build only the contents of my JavaScript modules
    • these files are using ES6 module syntax, JSX, TypeScript, later JavaScript version language features
    • these files need to be transpiled to what JavaScript that a browser understands
  • I do NOT want my build process to build my non-module JavaScript files
  • note: my config only does type-checking, but i still prefer that only errors in my JavaScript modules break my Webpack build

tsconfig.dts.json

{
  // "extends"
  // use all the settings in tsconfig.json
  // - but override any of those settings with what's in here
  // - and use any additional settings in here
  "extends": "./tsconfig.json",
  "compilerOptions": {
    // i DO want to generate files--so override noEmit from tsconfig.json
    "noEmit": false,
    // i still don't want to generate files if an error occurs though
    "noEmitOnError": true,
    "declaration": true,
    "declarationDir": "./wwwroot/app/dist/types",
    // i specifically want to generate ONLY declaration files
    "emitDeclarationOnly": true
  },
  // only consider the folders/files under "wwwroot/app/src"
  "include": [ "wwwroot/app/src/**/*" ],
  // WITHIN THE INCLUDE, do not process any declaration files under "wwwroot/app/src"
  "exclude": [ "wwwroot/app/src/**/*.d.ts" ]
}

This tsconfig.dts file is for my actual build process

webpack.config.js

const path = require("path");
var appPath = __dirname;

module.exports = (env, argv) => {
    var config = {
        context: appPath,
        entry: {
            // the entry point of my modules--the root module
            bundle: ["./wwwroot/app/src/index.js"]
        },
        output: {
            // remove old build output
            clean: true,
            // location for the build output
            path: path.resolve(appPath, "wwwroot/app/dist/bundle"),
            // name of output file--[name] => "bundle.js" (the entry name)
            filename: "[name].js",
            // just looking back at this now, maybe i don't have a need for publicPath
            // https://webpack.js.org/guides/public-path/
            publicPath: "~/wwwroot/app/dist/bundle/",
            // my root module (entry) exports an API--expose it as a global variable
            // since my target is browsers, Webpack exposes my API on window.ClientApp
            library: {
                name: "ClientApp",
                type: "var"
            }
        },
        optimization: {
            // for caching, separate my code from vendor code (e.g. React)
            // so i'll have a "bundle.js" for my code and "vendors.js" for React code
            // - this way users don't re-download React when they load my site after
            //   deploying an update--they just update my new code
            // - (the vendor code is much larger)
            splitChunks: {
                chunks: "all",
                cacheGroups: {
                    vendors: {
                        test: /[\\/]node_modules[\\/]/,
                        name: "vendors"
                    }
                }
            }
        },
        module: {
            rules: [
                // use babel to transpile JavaScript, JSX, and TypeScript
                {
                    // transpile all files with these extensions (.js,.jsx,.ts,.tsx)
                    test: /\.(ts|js)x?$/,
                    exclude: /node_modules/,
                    use: { loader: "babel-loader" }
                },
                // i will surround content like the following example--please remove it:
                //  - `/* webpack-strip-code-block:start */`
                //  - var example = "remove this line of code";
                //  - `/* webpack-strip-code-block:end */`
                {
                    test: /\.(ts|js)x?$/,
                    include: /[\\/]wwwroot[\\/]app[\\/]src[\\/]index/,
                    use: {
                        loader: "webpack-strip-block",
                        options: {
                            start: "webpack-strip-code-block:start",
                            end: "webpack-strip-code-block:end"
                        }
                    }
                }
            ]
        },
        // when resolving paths of module import statements, process files with
        // all these file extensions
        resolve: {
            extensions: [".js", ".jsx", ".ts", ".tsx"]
        }
    };

    // i want source mapping for browser debugging--but only for dev builds
    if (argv.mode === "development") { config.devtool = "eval-source-map"; }

    return config;
};
⚠️ **GitHub.com Fallback** ⚠️