Jeg opdagede først mine rådne npm scripts, da en ny kollega prøvede at køre dem
Jeg kan stadig huske den lidt for lange pause i mødelokalet, da en ny kollega sagde: “Jeg prøvede bare at køre npm test, men… hvad er npm run QA_full_old?”. Jeg vidste godt, vi havde et problem, men det var først dér, det var pinligt.
Hvis du også har siddet med en package.json med 25 scripts og nul overblik, så er det den situation, vi rydder op i nu.
Hvad vi prøver at bygge op med npm scripts
Lad os definere problemet kort, som hvis det var et lille issue i et repo:
- Mål: Et sæt npm scripts som er nemme at forstå, driftssikre og genbrugelige i både lokal udvikling og CI.
- Input: Et typisk JS/TS projekt (frontend, backend eller begge dele),
npmog en tom eller rodetscripts-sektion. - Output: Få, klare kommandorer: dev, build, test, lint, format, typecheck, plus et par hjælpe-scripts.
Resten er bare details.
Principper: sådan dør dine npm scripts ikke langsomt
Hvis du vil undgå at dit setup rådner, har jeg tre regler, jeg selv holder mig til.
1. Én indgang pr. aktivitet
For hver aktivitet, du forventer folk bruger ofte, skal der kun være én “officiel” kommando:
npm run devtil lokal udviklingnpm testtil testsnpm run linttil lintnpm run buildtil buildnpm run typechecktil TypeScriptnpm run formattil Prettier
Alt andet (f.eks. specielle CI-varianter eller engangsscripts) må gerne findes, men skal være tydeligt markeret med mere tekniske navne, f.eks. test:ci eller lint:fix.
2. Scripts kalder scripts, ikke bin-filer direkte
Mange starter sådan her:
"scripts": {
"test": "jest",
"lint": "eslint src"
}
Det virker, men når du skal have flere varianter, bliver det hurtigt rodet. Jeg foretrækker at have “rå” scripts og “officielle” scripts:
"scripts": {
"test": "npm run test:unit",
"test:unit": "jest",
"lint": "npm run lint:check",
"lint:check": "eslint src"
}
Fordelen er, at du kan ændre implementationen af test:unit uden at røre ved den “officielle” indgang (npm test).
3. Klare navne og navngivningsmønster
Jeg bruger typisk dette mønster:
- Basenavn er det, folk skal huske:
dev,build,test,lint,format,typecheck. - Varianter får kolon:
test:unit,test:watch,lint:fix,build:prod. - Intern/one-off får gerne længere navne:
dev:mock-api,build:analyze-bundle.
Pointen er, at du kan skimme listen og hurtigt se, hvad der er “core” og hvad der er ekstra.
Minimumssæt: frontend og backend uden at overgøre det
Jeg tager udgangspunkt i et ret typisk setup: TypeScript, ESLint, Prettier, Jest/Vitest og enten en bundler eller Node backend.
Basis scripts til et frontend-projekt
Eksempel med Vite, ESLint, Prettier og TypeScript:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:preview": "vite preview",
"test": "npm run test:unit",
"test:unit": "vitest run",
"test:watch": "vitest",
"lint": "npm run lint:check",
"lint:check": "eslint src --ext .ts,.tsx,.js,.jsx",
"lint:fix": "eslint src --ext .ts,.tsx,.js,.jsx --fix",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"typecheck": "tsc --noEmit",
"validate": "npm run lint && npm run typecheck && npm test"
}
}
validate er det script, jeg bruger både lokalt før jeg committer og i CI. Så skal du kun vedligeholde ét sted.
Basis scripts til et backend-projekt
Eksempel med Node, TypeScript, Jest, ESLint og Prettier:
{
"scripts": {
"dev": "nodemon --watch src --exec ts-node src/index.ts",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/index.js",
"test": "npm run test:unit",
"test:unit": "jest",
"lint": "npm run lint:check",
"lint:check": "eslint src --ext .ts,.js",
"lint:fix": "eslint src --ext .ts,.js --fix",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"typecheck": "tsc --noEmit",
"validate": "npm run lint && npm run typecheck && npm test"
}
}
Hvis du vil se mere basis-setup med Node og tooling, er der fx artikler om debugging og fejlbudgetter, som spiller godt sammen med et stabilt scripts-setup.
Pre- og post-scripts: hvornår hjælper de, og hvornår gør de ondt?
npm understøtter pre<scriptNavn> og post<scriptNavn>. Det gør det fristende at lægge logik “magisk” ind, men det kan også gemme vigtige ting væk.
Gode brugsscenarier
- Enkle forberedelser der altid skal ske før en opgave, f.eks. rydde en mappe:
"scripts": {
"prebuild": "rimraf dist",
"build": "vite build"
}
- Automatisk formatering før commit kombineret med tools som husky/lint-staged (kommer vi til om lidt).
Hvor det går galt
Jeg undgår pre/post scripts til ting, der:
- ændrer adfærd for en kommando, uden at det er tydeligt
- kan fejle på måder, der er svære at forstå for nye udviklere
- kun giver mening i CI, men kører lokalt (eller omvendt)
Eksempel på noget, jeg har fortrudt:
"pretest": "npm run build",
"test": "jest"
Det betød, at npm test pludselig tog meget længere tid, og fejlede på grund af build-problemer, selv når jeg bare ville køre en hurtig test. I dag ville jeg hellere have:
"scripts": {
"test": "jest",
"test:ci": "npm run build && npm test"
}
Cross-platform scripts: hvorfor cross-env og npm-run-all findes
Hvis du kun har macOS i projektet, opdager du tit først problemerne, når den første Windows-bruger joiner.
De to klassiske problemer
- Environment-variabler sat direkte i scripts, f.eks.
NODE_ENV=production. - Shell-specifik syntaks som
&&,||, globbing ellerrm.
cross-env til environment-variabler
cross-env gør det samme script brugbart på tværs af OS:
npm install --save-dev cross-env
"scripts": {
"build": "cross-env NODE_ENV=production vite build"
}
Det ser lidt tungere ud, men du slipper for specielle build:windows-varianter.
npm-run-all eller concurrently til flere tasks
Hvis du har brug for at køre flere ting på én gang, så lad være med at skrive shell-magi direkte. Brug et værktøj.
npm-run-all er god til sekvenser og simple parallelle jobs:
npm install --save-dev npm-run-all
"scripts": {
"dev": "run-p dev:client dev:server",
"dev:client": "vite",
"dev:server": "nodemon src/server.ts"
}
concurrently er mere fleksibel og kan fx farvekode output:
npm install --save-dev concurrently
"scripts": {
"dev": "concurrently "npm run dev:client" "npm run dev:server""
}
Begge værktøjer er lavet til at være cross-platform, i modsætning til hjemmerullede &&/;-kombinationer.
Git hooks uden smerte: husky + lint-staged
Pre-commit hooks kan være fantastiske. De kan også blive til et lille mareridt, hvis de tager 60 sekunder hver gang, du vil committe en stavefejl.
Jeg går efter to ting:
- Kun de ændrede filer formateres/lintes.
- Samme værktøjer og scripts som resten af projektet bruger.
Hurtigt setup med husky og lint-staged
Installer først:
npm install --save-dev husky lint-staged
npx husky install
Tilføj i package.json:
{
"scripts": {
"prepare": "husky install"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint",
"prettier --write"
],
"*.{json,md,css,scss}": [
"prettier --write"
]
}
}
Opret så en pre-commit hook:
npx husky add .husky/pre-commit "npx lint-staged"
Nu vil et commit gøre to ting:
- Køre ESLint og Prettier på de filer, du faktisk committer.
- Afvise committet, hvis lint fejler.
Det matcher godt med et manuelt npm run lint og npm run format, men forstyrrer ikke din normale udviklingscyklus alt for meget. Hvis du vil nørde videre med hooks, kan du også koble det sammen med fx feature flags og deployment-strategier, som vi har været inde på i andre artikler på Coding Class.
Samme scripts lokalt og i CI
Et simpelt princip, der sparer meget tid: CI skal bruge de samme scripts som udviklere.
Eksempel på et simpelt GitHub Actions-udsnit til et frontend-projekt:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run validate
- run: npm run build
Fordelen er, at når en udvikler siger “jeg kørte npm run validate lokalt, og det virker”, så ved du, at CI gør præcis det samme. Ingen skjulte test:ci-special-ting, der kun lever i YAML-filer.
En lille skabelon, du kan copy/paste til nye projekter
Her er et samlet eksempel på en package.json–scripts-sektion til et TypeScript frontend-projekt med Vite, Vitest, ESLint, Prettier, husky og lint-staged. Du kan justere efter behov, men strukturen holder sig typisk pæn, også når projektet vokser.
{
"scripts": {
"dev": "vite",
"build": "cross-env NODE_ENV=production vite build",
"build:preview": "vite preview",
"test": "npm run test:unit",
"test:unit": "vitest run",
"test:watch": "vitest",
"lint": "npm run lint:check",
"lint:check": "eslint src --ext .ts,.tsx,.js,.jsx",
"lint:fix": "eslint src --ext .ts,.tsx,.js,.jsx --fix",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"typecheck": "tsc --noEmit",
"validate": "npm run lint && npm run typecheck && npm test",
"prepare": "husky install"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint",
"prettier --write"
],
"*.{json,md,css,scss}": [
"prettier --write"
]
}
}
Hvis du også har backend i samme repo, kan du spejle mønsteret i en backend/-mappe med sin egen package.json, men stadig bruge samme navne: dev, build, test, lint, format, typecheck, validate. Det gør det også nemmere at forklare onboarding i README.
Har du brug for inspiration til at beskrive dit setup skarpt, kan du kigge på, hvordan vi i andre artikler om fx database-migrationer i teams eller async/await-fejl deler ansvaret op i klare trin.
Sådan kan du bygge videre
Hvis du først får en lille, rimelig ren struktur på dine npm scripts, er det ret nemt at bygge på uden at vælte det hele.
- Tilføj coverage-scripts:
test:coverageder kalder det samme testframework med en ekstra flag. - Lav rolige migrations-scripts til databaseændringer, f.eks.
db:migrateogdb:seed, der matcher dine deployments. - Indfør små helper-scripts til ting du alligevel ofte gør manuelt, f.eks.
cleanupder rydder build-mapper og caches.
Spørgsmålet er egentlig bare: næste gang du åbner package.json om 6 måneder, vil du så kunne gætte, hvad hvert eneste script gør, uden at prøve dig frem? Hvis ikke, er det måske nu, du får ryddet op.
Det bliver spændende at se, hvor meget mindre friktion du får i hverdagen, når dine npm scripts føles som små, stabile værktøjer i stedet for en gammel rodekasse.









Send kommentar
Du skal være logget ind for at skrive en kommentar.