Jeg opdagede først mine rådne npm scripts, da en ny kollega prøvede at køre dem

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), npm og en tom eller rodet scripts-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 dev til lokal udvikling
  • npm test til tests
  • npm run lint til lint
  • npm run build til build
  • npm run typecheck til TypeScript
  • npm run format til 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

  1. Environment-variabler sat direkte i scripts, f.eks. NODE_ENV=production.
  2. Shell-specifik syntaks som &&, ||, globbing eller rm.

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:

  1. Køre ESLint og Prettier på de filer, du faktisk committer.
  2. 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.jsonscripts-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:coverage der kalder det samme testframework med en ekstra flag.
  • Lav rolige migrations-scripts til databaseændringer, f.eks. db:migrate og db:seed, der matcher dine deployments.
  • Indfør små helper-scripts til ting du alligevel ofte gør manuelt, f.eks. cleanup der 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.

Brug værktøjer som cross-env for at sætte environment-variabler på tværs af Windows/Unix, eller dotenv-cli/node-scripts til at indlæse .env-filer. Undgå POSIX-specifikke kommandoer direkte i package.json; flyt kompleks logik til små JavaScript- eller shell-scripts som scripts kalder. Sæt hemmeligheder i din CI-platforms sekretlager i stedet for i repoet.
Hold et sæt 'kanoniske' root-scripts som orkestrerer workspaces og lad hver pakke have de samme navne for deres interne scripts. Brug npm/pnpm/yarn workspaces-kommandoer (f.eks. npm workspaces run eller npm run -w) til at køre scripts på tværs af pakker, og placer fælles devDependencies i root for at undgå duplikation. Det gør det let at køre build/test i hele repoet uden at gentage logikken.
Gør det trinvis: tilføj først nye 'officielle' wrapper-scripts som peger på de gamle implementeringer, opdater CI til at bruge de nye wrapper-scripts, og kør tests for at verificere. Når al CI og teamet bruger de nye navne, kan du fjerne eller deprecate de gamle scripts over et par releases med en klar changelog.
Lav et simpelt 'scripts:help' script der printer korte beskrivelser og eksempler, og hold en sektion i README med brugseksempler og nødvendige env-variabler. Kommentér ikke i package.json men brug dokumentation og små wrapper-scripts til at vise standardflowet - det er ofte hurtigere at forstå end at læse lange kommandoer.

Sara Vestergaard er selvlært kode-nørd, der stille og roligt er gået fra at rode med en enkelt HTML-side til at bygge små værktøjer, scripts og hjemmesider til sig selv og vennerne. Hun startede med at lave en simpel band-hjemmeside som teenager og opdagede, hvor tilfredsstillende det er, når noget, du har skrevet, pludselig lever på skærmen.

For Sara handler kodning ikke om store ord eller imponerende titler, men om meget konkrete problemer: den kedelige opgave, der tager for lang tid, den ven der mangler en lille porteføljeside, eller den liste, der burde sortere sig selv. Hun elsker at pille ting fra hinanden – også kode – for at se, hvad der egentlig foregår, og hun har brugt utallige aftener på at google fejlbeskeder, teste små eksempler og langsomt bygge sin forståelse op.

På Coding Class deler hun den tilgang videre. Hun skriver til dig, der gerne vil lære at kode ved at gøre det i praksis: små projekter, korte kodebidder og forklaringer, der hænger sammen med det, du faktisk sidder med på skærmen. Hun skærer ind til benet, viser typiske fejl og deres løsninger og giver altid et forslag til, hvordan du kan bygge en tand videre, når grundideen først virker.

Når hun ikke skriver til Coding Class eller nørkler med nye små projekter, hænger Sara på klatrevæggen, vander sine altanplanter eller spiller gamle Nintendo-spil. Men hun ender næsten altid tilbage ved tasterne – for der er altid endnu en lille ting, der kunne være smartere, hurtigere eller bare lidt sjovere at bruge.

Send kommentar

You May Have Missed