My JavaScript Build Tool Setup - egnomerator/misc GitHub Wiki
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
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
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"
}
}
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
{
"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 (**
)- source https://www.typescriptlang.org/tsconfig#include
- i want intellisense to know about everything ... almost
-
"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
- e.g.
- 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
- organize my folder structure to have files pertaining to my component in the same folder
{
// "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
{
// "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
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;
};