Scripts


https://docs.npmjs.com/cli/v11/using-npm/scripts
https://nodejs.org/api/cli.html
https://www.npmjs.com/package/nodemon
https://www.npmjs.com/package/dotenv-cli
https://pm2.io/docs/runtime/reference/ecosystem-file/
https://pm2.io/docs/runtime/features/watch-restart/
https://pm2.keymetrics.io/docs/usage/application-declaration/

1. Your Current Scripts#

{
  "local": "node index.js --mode local",
  "local:nodemon": "nodemon index.js --mode local",
  "local:dev": "dotenv -e .env.local nodemon index.js --mode local",
  "test": "node index.js --mode test",
  "test:nodemon": "nodemon index.js --mode test",
  "dev": "pm2 start index.js --name \"im-server\" --watch -o ../logs/out.log -e ../logs/error.log -- --mode dev",
  "dev:nodemon": "nodemon index.js --mode dev",
  "prod": "node index.js --mode prod",
  "lint": "eslint ."
}

2. Review#

overall#

结论:
    可以跑,但不够专业
    最大问题不是语法错误,而是职责和命名不清晰

主要问题:
    test 被用来启动 test mode app,而不是运行 automated tests
    local / local:nodemon / local:dev 重复
    dev 直接用 pm2 start 一长串参数,不利于维护
    pm2 --watch 不适合做普通 dev script,更不适合 production
    log path 写成 ../logs 容易依赖启动目录
    --mode 和 NODE_ENV / APP_ENV 的职责需要统一
    dotenv 命令最好加 -- 分隔 command

line by line#

Script Review
local 可以,但建议命名为 start:localserve:local
local:nodemon 可以,但和 local:dev 重复
local:dev 思路对,建议 dotenv -e .env.local -- nodemon ...
test 不建议。npm 生态里 test 应该运行测试
test:nodemon 不建议。容易和测试 watcher 混淆
dev 不建议直接塞 PM2 长命令,应该用 ecosystem.config.cjs
dev:nodemon 可以,建议改为默认 dev
prod 可以跑,但标准名应是 startstart:prod
lint 正确,但最好再加 lint:fixcheck

3. Naming Rule#

recommended script naming:
    dev:
        local development with watch/restart

    start:
        production-style start

    start:<env>:
        start app with a specific env

    test:
        run automated tests

    test:watch:
        run tests in watch mode

    lint:
        run eslint

    lint:fix:
        run eslint auto-fix

    check:
        run lint + test

    pm2:start:<env>:
        start with PM2 ecosystem file

    pm2:reload:<env>:
        zero-downtime reload when applicable
avoid:
    test = start app in test mode
    prod = production start only
    dev = PM2 process manager start
    long PM2 commands inside package.json

4. Environment Rule#

recommended:
    NODE_ENV:
        Node.js/library runtime mode
        usually local / test / production

    APP_ENV:
        business deployment environment
        local / dev / uat / staging / prod

    --mode:
        optional CLI override
        only use if the app really has CLI mode semantics

practical rule:
    prefer env variables for deployment
    use --mode only for local CLI-like behavior
example:
    NODE_ENV=production
    APP_ENV=prod
    LOG_LEVEL=info

Node.js 20+ with native –env-file#

{
  "scripts": {
    "dev": "node --env-file=.env.local --watch index.js",
    "dev:dev": "node --env-file=.env.dev --watch index.js",
    "start": "node index.js",
    "start:local": "node --env-file=.env.local index.js",
    "start:dev": "node --env-file=.env.dev index.js",
    "start:test-env": "node --env-file=.env.test index.js",
    "start:prod": "node index.js",
    "test": "node --test",
    "test:watch": "node --test --watch",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "check": "npm run lint && npm test",
    "pm2:start:dev": "pm2 start ecosystem.config.cjs --only im-server-dev",
    "pm2:start:prod": "pm2 start ecosystem.config.cjs --only im-server-prod",
    "pm2:reload:prod": "pm2 reload ecosystem.config.cjs --only im-server-prod --update-env",
    "pm2:stop": "pm2 stop im-server-dev im-server-prod"
  }
}
notes:
    dev:
        local watch mode

    start:
        standard production-style command

    start:test-env:
        if you really need to start app with test env
        do not use test for this

    test:
        real automated tests

    pm2:*:
        process manager scripts
        use ecosystem file

compatible with dotenv-cli + nodemon#

{
  "scripts": {
    "dev": "dotenv -e .env.local -- nodemon index.js",
    "dev:dev": "dotenv -e .env.dev -- nodemon index.js",
    "start": "node index.js",
    "start:local": "dotenv -e .env.local -- node index.js",
    "start:dev": "dotenv -e .env.dev -- node index.js",
    "start:test-env": "dotenv -e .env.test -- node index.js",
    "start:prod": "node index.js",
    "test": "node --test",
    "test:watch": "node --test --watch",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "check": "npm run lint && npm test",
    "pm2:start:dev": "pm2 start ecosystem.config.cjs --only im-server-dev",
    "pm2:start:prod": "pm2 start ecosystem.config.cjs --only im-server-prod",
    "pm2:reload:prod": "pm2 reload ecosystem.config.cjs --only im-server-prod --update-env"
  }
}
dotenv-cli:
    use -- before the command
    clearer parsing when command also has flags

6. nodemon#

install#

npm install -D nodemon dotenv-cli

nodemon.json#

{
  "watch": [
    "index.js",
    "src"
  ],
  "ext": "js,json",
  "ignore": [
    "node_modules",
    "logs",
    "coverage",
    "dist"
  ],
  "signal": "SIGTERM"
}
why:
    avoid watching node_modules / logs
    avoid restart loop when app writes logs
    keep package.json script short

7. PM2#

why ecosystem file#

PM2 command line is okay for one-off test:
    pm2 start index.js --name im-server

professional setup:
    ecosystem.config.cjs
    app name
    env variables
    log paths
    watch ignore
    instances / exec_mode

ecosystem.config.cjs#

module.exports = {
  apps: [
    {
      name: "im-server-dev",
      script: "index.js",
      exec_mode: "fork",
      instances: 1,
      watch: ["index.js", "src"],
      ignore_watch: ["node_modules", "logs", "coverage"],
      out_file: "../logs/im-server-dev-out.log",
      error_file: "../logs/im-server-dev-error.log",
      merge_logs: true,
      env: {
        NODE_ENV: "production",
        APP_ENV: "dev",
        LOG_LEVEL: "debug"
      }
    },
    {
      name: "im-server-prod",
      script: "index.js",
      exec_mode: "cluster",
      instances: "max",
      watch: false,
      out_file: "../logs/im-server-prod-out.log",
      error_file: "../logs/im-server-prod-error.log",
      merge_logs: true,
      env: {
        NODE_ENV: "production",
        APP_ENV: "prod",
        LOG_LEVEL: "info"
      }
    }
  ]
};
notes:
    dev:
        watch can be enabled if this VM is used for development

    prod:
        watch should usually be false
        deploy should restart/reload intentionally

    cluster:
        only use when app is stateless and safe for multi-process

    logs:
        for VM deployment only
        container/K8S should prefer stdout/stderr

commands#

npm run pm2:start:dev
npm run pm2:start:prod
npm run pm2:reload:prod
pm2 logs im-server-prod
pm2 describe im-server-prod

8. Mode Parsing#

when to use –mode#

--mode 适合:
    本地手动切换启动模式
    同一个入口执行不同 command behavior
    CLI-style app
    临时覆盖 APP_ENV

--mode 不适合:
    作为生产环境唯一配置来源
    替代 NODE_ENV
    替代 secret/config injection
    每个 npm script 都必须带一个 mode
recommended precedence:
    command line --mode
    APP_ENV
    NODE_ENV
    default local

why:
    --mode:
        manual override

    APP_ENV:
        deployment environment

    NODE_ENV:
        runtime/library optimization mode

    default:
        local developer experience

parse –mode#

function getArgValue(name) {
  const prefix = `--${name}=`;
  const arg = process.argv.find((item) => item.startsWith(prefix));

  if (arg !== undefined) {
    return arg.slice(prefix.length);
  }

  const index = process.argv.indexOf(`--${name}`);

  if (index >= 0) {
    return process.argv[index + 1];
  }

  return undefined;
}

export const runtimeMode =
  getArgValue("mode") ??
  process.env.APP_ENV ??
  process.env.NODE_ENV ??
  "local";
supported:
    node index.js --mode local
    node index.js --mode=local

better:
    app config reads APP_ENV
    --mode only overrides when needed

avoid:
    every script depends on --mode
    mode and env conflict without clear precedence

validate mode#

const allowedModes = new Set(["local", "dev", "test", "uat", "staging", "prod"]);

if (!allowedModes.has(runtimeMode)) {
  console.error(`invalid mode: ${runtimeMode}`);
  process.exit(1);
}
why:
    fail fast on typo
    avoid app silently running with wrong config

bad:
    node index.js --mode prdo
    app starts with unexpected mode

use mode to load config#

import { config as loadDotenv } from "dotenv";

const mode = runtimeMode;

if (mode === "local") {
  loadDotenv({ path: ".env.local" });
}

if (mode === "dev") {
  loadDotenv({ path: ".env.dev" });
}

if (mode === "test") {
  loadDotenv({ path: ".env.test" });
}
practical rule:
    local/dev/test can load .env files
    prod should normally use platform-injected environment variables
    do not load .env.prod from git

scripts with mode#

{
  "scripts": {
    "dev": "dotenv -e .env.local -- nodemon index.js --mode local",
    "start:local": "dotenv -e .env.local -- node index.js --mode local",
    "start:dev": "dotenv -e .env.dev -- node index.js --mode dev",
    "start:test-env": "dotenv -e .env.test -- node index.js --mode test",
    "start:prod": "node index.js --mode prod"
  }
}
note:
    this is acceptable if the app genuinely uses --mode
    still keep test for automated tests:
        "test": "node --test"

Docker#

docker run --rm \
  -e NODE_ENV=production \
  -e APP_ENV=prod \
  order-api:1.0.0
docker run --rm \
  -e NODE_ENV=production \
  -e APP_ENV=prod \
  order-api:1.0.0 \
  node index.js --mode prod
recommendation:
    Docker/K8S should prefer APP_ENV
    --mode in container command is optional
    avoid duplicating APP_ENV=prod and --mode dev

Kubernetes#

env:
  - name: NODE_ENV
    value: production
  - name: APP_ENV
    value: prod
command:
  - node
args:
  - index.js
  - --mode
  - prod
recommendation:
    use env for deployment environment
    use args only when this service is intentionally CLI-like

PM2#

module.exports = {
  apps: [
    {
      name: "im-server-prod",
      script: "index.js",
      args: "--mode prod",
      env: {
        NODE_ENV: "production",
        APP_ENV: "prod",
        LOG_LEVEL: "info"
      }
    }
  ]
};
PM2 rule:
    if args has --mode prod, env.APP_ENV should also be prod
    do not set args --mode dev with APP_ENV prod

bad vs good#

bad:
    "test": "node index.js --mode test"

good:
    "test": "node --test"
    "start:test-env": "dotenv -e .env.test -- node index.js --mode test"

bad:
    APP_ENV=prod node index.js --mode dev

good:
    APP_ENV=prod node index.js

bad:
    production secrets selected by --mode only

good:
    production secrets injected by platform
    --mode only selects behavior/config profile

9. Migration From Your Scripts#

old:
    local
new:
    start:local

old:
    local:nodemon / local:dev
new:
    dev

old:
    test
new:
    start:test-env

old:
    test:nodemon
new:
    dev:test-env or remove

old:
    dev with pm2 long command
new:
    pm2:start:dev + ecosystem.config.cjs

old:
    prod
new:
    start / start:prod
{
  "scripts": {
    "dev": "dotenv -e .env.local -- nodemon index.js",
    "start": "node index.js",
    "start:local": "dotenv -e .env.local -- node index.js",
    "start:dev": "dotenv -e .env.dev -- node index.js",
    "start:test-env": "dotenv -e .env.test -- node index.js",
    "start:prod": "node index.js",
    "test": "node --test",
    "test:watch": "node --test --watch",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "check": "npm run lint && npm test",
    "pm2:start:dev": "pm2 start ecosystem.config.cjs --only im-server-dev",
    "pm2:start:prod": "pm2 start ecosystem.config.cjs --only im-server-prod",
    "pm2:reload:prod": "pm2 reload ecosystem.config.cjs --only im-server-prod --update-env",
    "pm2:stop": "pm2 stop im-server-dev im-server-prod"
  }
}
dependencies:
    npm install express

devDependencies:
    npm install -D nodemon dotenv-cli eslint @eslint/js

11. Checklist#

scripts:
    dev is local watch
    start is normal start
    test is automated tests
    lint exists
    check exists
    PM2 commands use ecosystem file

env:
    .env.example committed
    .env.local ignored
    APP_ENV / NODE_ENV precedence documented
    production config injected by platform

logs:
    VM PM2 logs configured
    logrotate configured if writing files
    container/K8S uses stdout/stderr