Keentune

Package Management curriculum

22 chapters
·
137 concepts
·
free
Everything the adaptive question bank can teach and test in Package Management, from foundations through advanced practice. Work through it in order, or start practising and let the questions find your level.
New here? Read the Package Management guide
A free 16-minute primer — the mental model, the mistakes beginners make, and what to practise first.
A. Packages, registries and what a package manager does
a directory with a package.json, distributed as one gzipped tarball
the registry serves one JSON document per name listing every version and its tarball URL
a published name@version can be deprecated but its contents can never be rewritten, which is what makes pinning safe
npm, pnpm and Yarn are competing clients of the same registry HTTP API
you declare direct dependencies; the resolver computes the whole transitive closure
resolve a graph, fetch tarballs, then link them into node_modules
a local install is project state a lockfile reproduces; a global install is machine state nothing records
B. package.json: identity and metadata
every other field is optional, and both are required only to publish
lowercase, URL-safe, at most 214 characters, no leading dot or underscore
@scope/name namespaces a package so the unscoped name being taken does not block you
files is an allowlist and overrides .npmignore; with neither, .gitignore is used
package.json, README and LICENSE always ship; .git and node_modules never do
"private": true makes npm publish refuse, which is how a monorepo root protects itself
"type": "module" reinterprets every .js file in the package as ESM
C. Dependency kinds and who installs what
what your code needs at run time, installed transitively by every consumer
a consumer of your package never installs your devDependencies
--omit=dev skips them, which is why a deploy image must not need a build-only tool
"I plug into the copy you already have", so the host supplies the instance
an optionalDependencies failure does not fail the install, so the code must handle absence
marks a peer as not required, suppressing the warning when it is missing
a build tool in dependencies bloats every consumer; a runtime library in devDependencies breaks them
D. Semantic versioning: what a number promises
breaking change, backward-compatible feature, backward-compatible fix
under 0.y.z anything may break; the minor slot carries the breaking changes
only a documented, depended-on contract makes a change major
1.0.0-beta.1 has LOWER precedence than 1.0.0, and identifiers compare field by field
a +build.5 suffix is ignored entirely when comparing precedence
semver is a publisher's claim, not an enforced fact, so a caret range is trust and a lockfile is proof
E. Ranges and how a resolver picks a version
^1.2.3 allows any change that does not alter the leftmost non-zero digit
^0.2.3 allows 0.2.x only; it will not take 0.3.0
~1.2.3 allows patch bumps only, while ~1.2 allows the whole 1.2 minor
a bare 1.2.3 pins exactly; * and an empty range accept anything published
a space means AND, || means OR, and 1.2.3 - 2.3.4 is an inclusive hyphen range
the resolver takes the highest published version the range allows, not the newest overall
a range never matches a prerelease unless the range itself names one
F. Lockfiles
an exact reproducible tree: every resolved version, its URL and its integrity hash
apps commit it; a library's lockfile is never installed by its consumers
the stored subresource-integrity hash is verified on download, so a swapped tarball fails
when the range no longer covers the pinned version the strict install errors rather than guessing
resolve a lockfile conflict by re-running the install, never by hand-editing the JSON
package-lock.json, pnpm-lock.yaml and yarn.lock do not interoperate, and two in one repo means two trees
G. Installing: npm install versus npm ci, cache and offline
npm install may add packages and rewrite the lockfile, so its result depends on when you ran it
npm ci wipes node_modules, installs exactly the lockfile, and never writes it back
it fails outright when the lockfile is absent or out of sync with the manifest
--save-dev, --save-exact and --no-save decide what, if anything, lands in the manifest
the cache keys on integrity hash, which is what makes a repeat install work offline
deleting node_modules and clearing the cache fix different failures
H. node_modules, the resolution algorithm and hoisting
a bare specifier is looked up in ./node_modules, then each parent directory, to the filesystem root
a directory resolves through its package.json main field, then falls back to index.js
CommonJS tries .js, .json, .node; ESM requires the exact file extension
npm and Yarn lift shared dependencies to the top level so one copy serves many dependents
importing a package you never declared works only by accident of hoisting, and breaks on someone else's tree
incompatible ranges force a nested second copy, so two versions of one library coexist
two copies of a library means separate module state, so instanceof and singletons fail
I. Peer dependencies and peer conflicts
a plugin must share the host's single instance instead of bundling a second one
npm 7+ installs peers automatically; npm 6 only printed a warning
two packages demanding incompatible peer ranges is a real conflict, not a tooling bug
it restores the ignore-and-warn behavior and installs a pair that may genuinely not work together
the durable fix is a published peer range that covers the host version, not a local flag
J. Overrides, dedupe and aliases
npm's overrides forces a version onto a transitive dependency you do not directly control
npm overrides, Yarn resolutions and pnpm pnpm.overrides differ in syntax and scope
forcing a version can violate the dependent's declared range, so upgrading the parent is the real fix
moving duplicated packages up the tree so one copy satisfies several dependents
npm i x@npm:y@1 installs one package under a different local name, e.g. to run two majors side by side
K. Scripts and lifecycle hooks
npm run prepends node_modules/.bin, so a script calls a local CLI by bare name
pre<name> and post<name> run automatically around any script you define
prepare, prepublishOnly, prepack and postinstall fire on install and publish events, not on demand
on a local install and before packing, which is what builds a package installed straight from git
npm test and npm start need no run; every other script does
arguments after -- go to the script, not to npm itself
any installed package can execute code on your machine, and --ignore-scripts is the mitigation
L. bin, npx and executables
bin maps a command name to a file, which npm links into node_modules/.bin on install
a bin script needs a #!/usr/bin/env node line, and npm sets the executable bit when packing
npx runs the project-local binary when one exists and only then fetches into a temporary cache
a mistyped package name can silently execute a stranger's package; the confirmation prompt exists for that
M. Workspaces and monorepos
a glob list in the root manifest turns sibling folders into locally linked packages
the whole repo shares one lockfile and one install, which is what keeps versions aligned
a dependency on a sibling resolves to that folder, so an edit is live with no publish step
pnpm and Yarn's workspace:* refuses to resolve from the registry and is rewritten at publish time
-w <name> targets one workspace and --workspaces targets them all
two workspaces pinning different majors of one library reintroduces duplicate instances
N. npm, pnpm, Yarn and Corepack
one global store hard-linked into each project, so a second copy of a version costs almost no disk
only declared dependencies are linked into a package, so a phantom import fails loudly instead of working
PnP removes node_modules and resolves through a manifest, which breaks tools that walk real directories
"packageManager": "pnpm@9.1.0" pins the tool version the repo expects
Corepack proxies pnpm/yarn to the pinned version, so contributors do not silently use different tools
npm ci, pnpm install --frozen-lockfile and yarn --immutable are the same intent, spelled three ways
O. Publishing
npm pack --dry-run lists exactly what will ship, which is how you catch a missing dist or a leaked .env
a scoped package publishes restricted unless you pass --access public
the registry rejects a republish of an existing version, and npm version bumps and git-tags in one step
publishing moves the latest tag unless you publish under another tag, which is how a beta avoids becoming the default install
unpublishing is restricted after a short window, so npm deprecate is the tool for a bad release
a signed attestation ties the tarball to the CI run, repository and commit that built it
.npmrc carries the registry, per-scope registries and auth, so a leaked automation token is a publish
P. The exports field and dual packaging
once exports exists, any subpath it does not list is unimportable even though the file is in the tarball
import, require, node and default select a different file per consumer
conditions match top to bottom, so default must be last and types must come first
named subpaths and a ./* pattern replace publishing a browsable folder tree
#-prefixed specifiers give a package private internal aliases that only resolve inside itself
shipping CJS and ESM builds can load both copies at once, giving one library two separate states
bolting exports onto an existing package breaks every consumer that deep-imported a file
Q. CommonJS, ESM and interop failure modes
.mjs is always ESM and .cjs is always CommonJS, whatever "type" says
current Node can require() a synchronous ES module, so the classic ERR_REQUIRE_ESM no longer fires; a graph with top-level await still throws ERR_REQUIRE_ASYNC_MODULE, and publishing ESM-only remains a breaking change for older consumers
await import() is how CommonJS code loads an ESM-only dependency
module.exports = x surfaces as the default import, which is where the stray .default comes from
Node statically detects them, and detection fails on exports assigned dynamically
using it forces the whole module graph to ESM, which can be what makes a package unrequireable
R. Audit and vulnerability handling
it submits the resolved dependency tree to an advisory service and reports the advisories that match
a critical advisory in a build-only dev dependency may be unreachable from anything you ship
--force accepts semver-major upgrades, so it can break the build it claims to fix
a nested dependency cannot move until its parent's range moves, or you add an override
auditing production dependencies only is what tells you about your actual attack surface
when no fixed version exists, the options are a fork, an override, or removing the dependency
S. Supply-chain security
a near-miss package name banking on a mistyped install or a copy-pasted command
a public package matching an internal name can win resolution unless scopes and registries are pinned
disabling lifecycle scripts removes the most common install-time execution path
a committed lockfile with integrity hashes is what detects a tarball swapped after the fact
the common incident shape: a trusted name, a new patch version, a malicious install script
a generated CycloneDX or SPDX inventory is how you answer "do we ship the affected version?" in minutes
T. Bundling: what a bundler does and which tool does it
it walks the module graph from an entry point and emits fewer, loadable output files
build tools resolve exports too, adding conditions like browser, worker and development
the fast HMR path and the optimized build are different pipelines that can disagree
esbuild and SWC are fast transformers, Rollup targets libraries, webpack targets apps, Vite orchestrates
a library marks its peers external so the consuming app supplies exactly one copy
U. Tree shaking, code splitting and source maps
dead-code elimination depends on statically analyzable import/export, so a CJS build defeats it
"sideEffects": false lets a bundler drop unused modules, and mislabeling it makes an imported CSS file disappear
a re-export index can pull in a whole package when purity cannot be proven
a dynamic import() becomes its own chunk fetched on demand instead of up-front
splitting rarely-changing dependencies out so a deploy does not invalidate every cached file
minifying rewrites the code; gzip and brotli happen at transport and are not a substitute
they map generated code back to source, and shipping them publicly publishes your source
V. Transpilers, targets and Node versions
syntax is rewritten at build time, but a missing runtime API needs a polyfill shipped with the code
a lower target emits more helper code, so an over-conservative target costs bytes and speed
one shared query drives Babel, Autoprefixer and the bundler so they cannot disagree about support
output syntax, ambient API declarations and module format are three independent settings
TypeScript checks nothing at run time, and tsc emits files rather than bundling them
engines declares the supported Node and npm range, and strictness decides whether it is advice or a hard stop
an .nvmrc or equivalent pins the runtime itself, which engines only documents and does not install
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All about Package Management practice
Also on your phone
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.
© 2026 SportaApp LLC