Appearance
The Modern JavaScript Toolchain
JavaScript tooling feels unusually crowded because several independent jobs are often bundled behind one command. A useful mental model assigns each tool a responsibility:
- The runtime executes JavaScript.
- The package manager resolves and installs dependencies.
- The type checker analyzes TypeScript relationships.
- The linter analyzes suspicious or inconsistent source patterns.
- The formatter chooses source layout.
- The test runner executes verification code.
- The development server serves source and coordinates fast updates.
- The bundler transforms a module graph into deployable assets.
Node and Bun combine several roles, as do framework CLIs, but the underlying distinctions remain useful when diagnosing a failure.
Package metadata is the project entry point
A small browser project might declare:
json
{
"name": "transaction-dashboard",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"check": "tsc --noEmit",
"lint": "eslint .",
"test": "vitest run",
"build": "vite build"
},
"dependencies": {
"react": "...",
"react-dom": "..."
},
"devDependencies": {
"eslint": "...",
"typescript": "...",
"vite": "...",
"vitest": "..."
}
}Scripts provide stable project commands independent of globally installed binaries. Runtime packages belong in dependencies; build, checking, and testing tools usually belong in devDependencies. Applications should commit one lockfile produced by their selected package manager so installations resolve consistently.
Node with npm is the conservative baseline because documentation and package compatibility are broad. pnpm and Yarn are alternative package managers with different storage and workspace behavior. Bun combines a runtime, package manager, test runner, and bundling capabilities. Choose deliberately, avoid mixing lockfiles, and do not assume package-manager choice changes JavaScript semantics.
Vite connects source modules to the browser
During development, Vite serves source through native ESM and transforms files on demand. It resolves package imports and provides hot module replacement, allowing affected modules to update without a full page reload.
For production, vite build follows the dependency graph and produces optimized static assets. This is where bundling, code splitting, asset hashing, and environment replacement happen.
Vite transforms TypeScript syntax but does not perform complete type checking. Keep tsc --noEmit separate. The same distinction applies to many fast build tools: successful transformation means the syntax could be emitted, not that the program satisfies its types.
Browser-facing environment variables are bundled into client code. Vite exposes selected values through import.meta.env and conventionally only exposes variables with its public prefix. Anything delivered to the browser is observable by users; never place server secrets there.
ESLint finds source-level problems
Modern ESLint configuration uses a JavaScript configuration array:
js
// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
"no-console": "off",
},
},
);React projects add the official or community plugins recommended by their chosen framework template. Some TypeScript rules require type information and are slower; enable them when their findings justify the cost.
A linter is strongest at detecting patterns: discarded promises, stale React effect dependencies, accidental fallthrough, unused variables, or unsafe TypeScript operations. Formatting rules create noisy configuration conflict, so many teams delegate layout to a formatter.
Prettier is an opinionated formatter. Other projects use Biome or a formatter built into another tool. The particular choice matters less than having one committed configuration and letting automation apply it. Do not hand-negotiate whitespace in review.
Tests occupy several layers
Node includes a test runner, while Vitest integrates closely with Vite projects. Jest remains common in established codebases. DOM component tests frequently use Testing Library, which encourages interaction through user-visible roles and labels.
Fast unit tests suit pure parsing and domain logic. Integration tests check modules together and may use a simulated DOM. Browser automation tools such as Playwright run the application in real browser engines and cover routing, layout-sensitive behavior, and complete user flows.
Use the cheapest layer that can observe the behavior. A formatting function does not need a browser; a login redirect may. Coverage is evidence of executed lines, not proof of meaningful assertions.
Automation should reproduce one local command
A useful local gate is:
sh
npm run check
npm run lint
npm test
npm run buildContinuous integration should run the same scripts on a clean install. Avoid a CI-only chain of hidden commands that developers cannot reproduce. Git hooks can shorten feedback but should not be the only enforcement because hooks can be skipped or misconfigured.
Keep dependency updates routine and review lockfile changes. Package lifecycle scripts execute code during installation, so dependencies are part of the software supply chain. Prefer well-maintained direct dependencies, remove unused packages, and use the package manager's audit information as one signal rather than a complete security verdict.
Frameworks add conventions above the toolchain
React is a UI library, not by itself a router, data framework, bundler, or deployment platform. Frameworks add opinions about routing, server rendering, data loading, caching, server/client boundaries, and build output. Those capabilities can be valuable, but learn which layer provides each one.
Generated starter projects are the best source for compatible initial configuration because plugin APIs and recommended settings change. Read what the template created: scripts, module mode, TypeScript settings, lint configuration, and deployment target. Once understood, remove unnecessary parts rather than accumulating tools by habit.
When a command fails, classify it before changing configuration:
text
Can the runtime load the module?
Can the package manager resolve it?
Can the transformer parse it?
Does the type checker accept it?
Does the linter object to a pattern?
Does the test expose incorrect behavior?That classification is the durable skill. Individual tools will change; the responsibilities and boundaries they serve are much more stable.