# npm and Vite

Node.js, packages from npm, versions and the lock file, and a Vite project from npm create to a folder you can deploy.

Multimedia & Creative Technology · 3.1 Extra Webdev · Self-study · version 0.1.0 · 2026-10-14

Tags: npm, vite, javascript, terminal, security

Slides: <https://slides.mctantwerp.be/en/extra-webdev/02-npm-and-vite>

<!-- slide 2 -->

## After this lesson you can

- Install Node.js with a version manager, and pin the version of a project in .nvmrc
- Add, update and remove packages, and read package.json and the lock file
- Explain what ^ and ~ mean, and why you commit package-lock.json but never node_modules
- Judge a package before you install it, and spot the risks of install scripts and typos
- Start a Vite project, import packages, CSS and images, and build it for production
- Keep secrets out of your bundle, and read the errors npm and Vite throw at you

> **Explanation:** These skills come back in every web project you start from now on: a Tailwind setup, a React app, a site you deploy. Once npm and Vite click, the README of most frontend projects on GitHub reads like a recipe instead of a riddle.

<!-- slide 3 -->

## Agenda

1. Node.js
2. npm
3. Versions and the lock file
4. Scripts and npx
5. Choosing a package
6. Security
7. Vite
8. Importing things
9. Build and deploy
10. When it breaks

> **Explanation:** Chapters 1 to 6 are about Node.js and npm, and they hold for every JavaScript project, with or without Vite. Chapters 7 to 10 are Vite. If you already use npm daily, skim chapter 2 and start for real at chapter 3: the lock file and security are where most people have gaps.

<!-- slide 4 -->

## 1. Node.js

> **Explanation:** Up to now your JavaScript ran in one place: the browser, after a script tag. The tools in this deck need a second place to run JavaScript, on your own laptop. That is Node.js, and it is the first thing you install.

<!-- slide 5 -->

### Why a frontend dev needs it

- **npm**: Installs packages: code of other people, like a confetti effect or a date library.
- **Vite**: Serves your site while you work on it, and builds it for production.
- **The rest**: Tailwind, Prettier, ESLint, TypeScript: all JavaScript programs, all run on Node.js.

Node.js runs the tools on your laptop. Your site still runs in the browser of the visitor, who never needs Node.js. More in the [introduction on nodejs.org](https://nodejs.org/en/learn/getting-started/introduction-to-nodejs).

> **Explanation:** Node.js takes V8, the JavaScript engine of Chrome, and puts it in a program you start from the terminal: no window or document, but files, folders and the network. You will not write a Node.js server in this course. You use Node.js because your tools need it, the same way you need Python installed to run a Python script. Read the intro on nodejs.org if you want to know more about what Node.js itself can do.

<!-- slide 6 -->

### Node in your terminal

*hello.js*

```js
const name = 'Ada'
console.log(`Hello ${name}, from Node.js`)
```

```console
$ node -v
v24.22.0
$ npm -v
11.19.1
$ node hello.js
Hello Ada, from Node.js
```

**[The terminal](https://slides.mctantwerp.be/en/general/03-terminal)**: cd, ls and the other commands you need to get around.

> **Explanation:** node runs a JavaScript file, -v prints the version. npm comes with Node.js, you never install it separately. console.log in Node.js writes to the terminal, there is no DevTools console. Typing node without a file opens the REPL, a prompt where you type JavaScript line by line, handy to check something quickly. Leave it with Ctrl+C twice or .exit. If node -v says 'command not found', Node.js is not installed yet, or your terminal was opened before the install: close it and open a new one.

<!-- slide 7 -->

### Which version

| Line | Status in October 2026 | Use it? |
| --- | --- | --- |
| Node.js 26 | Current, becomes LTS on 28 October 2026 | Not yet for a project |
| Node.js 24 (Krypton) | Active LTS, fixes until April 2028 | **Yes, this one** |
| Node.js 22 (Jod) | Maintenance LTS, only security fixes until April 2027 | Only for an existing project |
| Node.js 25 and older odd lines | End of life | No |

LTS means Long Term Support: years of fixes, no surprises. Current gets the new features first, and the bugs that come with them. The dates are on the [releases page](https://nodejs.org/en/about/previous-releases).

> **Explanation:** The table is the situation on 14 October 2026. Node.js 26 becomes LTS on 28 October 2026, from then on both 24 and 26 are fine for a new project. From Node.js 27 the schedule changes: one major per year, and every major becomes LTS. The rule stays the same: take what nodejs.org labels LTS on the day you install, and check the dates on the releases page.

<!-- slide 8 -->

### A version manager

```console
# macOS
$ brew install fnm
# Windows
$ winget install Schniz.fnm
# then, on both
$ fnm install --lts
$ fnm default 24
```

Your portfolio from last year runs on Node 20, the new project on 24. A version manager like [fnm](https://github.com/Schniz/fnm) keeps both on your laptop and switches per project. One more step: the shell setup line from the fnm README goes in your `.zshrc` or PowerShell profile.

> **Explanation:** The installer from nodejs.org works, but it gives you one version for the whole laptop. On macOS it also tends to put global packages in a folder where you need sudo, and sudo with npm is a habit you do not want. nvm is the classic (macOS and Linux only), nvm-windows is a separate project for Windows. fnm works the same on all three, which is why it is the one in this deck.

<!-- slide 9 -->

### The version in your repo

*.nvmrc*

```bash
24
```

```console
$ echo 24 > .nvmrc
$ fnm use
Using Node v24.22.0
```

An `.nvmrc` in the root of your project says which Node.js it needs. Commit it. Whoever clones your repo, you on another laptop included, gets the right version with one command.

> **Explanation:** With --use-on-cd in the shell setup, fnm switches the moment you cd into a folder with an .nvmrc or .node-version file. Without it you type fnm use yourself. nvm reads .nvmrc too, with nvm use. The engines field in package.json says the same for npm, and npm warns when the version does not match. Netlify reads .nvmrc too, to pick the Node version for your build. On Windows, create the file in VS Code: Windows PowerShell writes UTF-16 with >, which some tools cannot read.

<!-- slide 10 -->

## 2. npm

> **Explanation:** npm is three things with one name: a command on your laptop, a registry with millions of packages, and the company behind it, part of GitHub since 2020. This chapter is about the command and the files it writes.

<!-- slide 11 -->

### npm init

```console
$ npm init -y
Wrote to ~/Projects/confetti/package.json:
{
  "name": "confetti",
  "version": "1.0.0",
  "scripts": { "test": "echo \"Error: no test specified\" && exit 1" },
 ...
}
```

`npm init` turns a folder into a project: it writes a `package.json`, where npm keeps track of the packages you install. Those come from the registry on [npmjs.com](https://www.npmjs.com), where anyone can publish and nobody reviews. Details in the [npm init docs](https://docs.npmjs.com/cli/commands/npm-init).

> **Explanation:** Package names with an @ in front, like @tailwindcss/vite, are scoped: the part before the slash is the organisation that publishes them. -y answers yes to every question npm init would otherwise ask. You can change everything later in the file itself, package.json is plain JSON. Your npm version can write a few more or fewer fields than shown here, that does not matter. For a Vite project you skip npm init: npm create vite writes a package.json for you.

<!-- slide 12 -->

### npm install

```console
$ npm install canvas-confetti
added 1 package, and audited 2 packages in 422ms
1 package is looking for funding
found 0 vulnerabilities
```

| What changed | Why |
| --- | --- |
| `node_modules/canvas-confetti/` | The code itself, downloaded from the registry |
| `package.json` | A new line under dependencies: `"canvas-confetti": "^1.9.4"` |
| `package-lock.json` | New file: the exact version that was installed, more in chapter 3 |

> **Explanation:** npm install, npm i and npm add are the same command. 'audited 2 packages' counts your own project too. The funding message is a request for donations from maintainers, npm fund lists them. To remove a package again: npm uninstall canvas-confetti, which also takes it out of package.json and the lock file.

<!-- slide 13 -->

### What package.json holds

```json
{
  "name": "my-site",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "dependencies": {
    "canvas-confetti": "^1.9.4"
  },
  "devDependencies": {
    "prettier": "^3.9.10",
    "vite": "^8.3.0"
  }
}
```

1. The name of your project. Vite also adds a `version` and `private: true`.
2. `import` and `export` instead of `require()`.
3. Commands with a short name: `npm run dev`.
4. Packages your site uses in the browser.
5. Tools you only need while you work.

> **Explanation:** The private field, which Vite adds next to a version, stops you from publishing your site to the npm registry by accident. type: module tells Node.js to treat .js files as ES modules, with import and export, instead of the older require(). Vite projects always have it. The scripts and the two dependency lists get their own slides further on. The full list of fields is in the package.json docs.

<!-- slide 14 -->

### dependencies and devDependencies

#### dependencies

```console
$ npm install canvas-confetti
```

Code that ends up in the browser of your visitor: a confetti effect, a slider, a date library, React.

#### devDependencies

```console
$ npm install -D prettier
```

Tools you only run on your laptop: Vite, Prettier, ESLint, TypeScript, the Tailwind plugin. `-D` is short for `--save-dev`.

> **Explanation:** Honest footnote: in a Vite project the split is mostly bookkeeping. Vite bundles whatever your code imports, wherever it is listed, and npm install installs both lists. The split matters for real when you publish a package (users only get your dependencies) and on a server with npm ci --omit=dev. Keep it tidy anyway: the next person reads package.json to understand what your project is made of.

<!-- slide 15 -->

### Drill: where does it go

**1. canvas-confetti, for a confetti burst when a form is sent.**

- **dependencies** ✓
- devDependencies
- Neither, run it once with npx

The confetti code runs in the browser of every visitor. That makes it a dependency.

**2. vite, the dev server and build tool of your project.**

- dependencies
- **devDependencies** ✓
- Neither, run it once with npx

Vite runs on your laptop and on the build server. Nothing of Vite itself ends up in dist, only what it built.

**3. create-vite, to start a new project.**

- dependencies
- devDependencies
- **Neither, run it once with npx** ✓

npm create vite runs it once to write the files of your project. After that you never need it again, so it does not belong in package.json.

**4. gsap, for the scroll animations on your portfolio.**

- **dependencies** ✓
- devDependencies
- Neither, run it once with npx

The animations run in the browser, so GSAP has to ship with the site.

**5. prettier, so your code is formatted the same way every time.**

- dependencies
- **devDependencies** ✓
- Neither, run it once with npx

Prettier only touches your source files. A visitor never runs it. In the project, so everyone formats with the same version.

**6. @tailwindcss/vite, the Tailwind plugin for Vite.**

- dependencies
- **devDependencies** ✓
- Neither, run it once with npx

The plugin generates CSS while Vite builds. The CSS ends up on the site, the plugin does not.

> **Explanation:** The third option is for tools you run once: a project generator does its work and you never need it again in that project. If you hesitate between the first two, ask yourself: does a visitor of the site run this code? Yes means dependencies. Tailwind is the odd one: its CSS ends up on the site, but the package itself only runs at build time, so it is a devDependency.

<!-- slide 16 -->

### node_modules

*.gitignore*

```bash
node_modules
dist
*.local
```

```console
$ du -sh node_modules
29M    node_modules
```

One package needs other packages, and those need more. A new Vite project asks for one package and npm adds fifteen. All of it lands in `node_modules`, and none of it goes in git: `npm install` rebuilds the folder from `package.json` and the lock file whenever you need it.

> **Explanation:** The 29 MB is a fresh Vite 8 project on a Mac: fifteen packages, for one devDependency. A React project with ESLint easily reaches a few hundred MB. node_modules also holds files that only work on your operating system: lightningcss-darwin-arm64 on a Mac with Apple silicon, another one on Windows. That is one more reason never to copy it to another laptop or put it in git.

<!-- slide 17 -->

### You clone the repo of a classmate and type npm run dev. You get: sh: vite: command not found. Why?

- A. Vite is not installed globally on your laptop
- B. node_modules is not in the repo, so nothing is installed yet
- C. The dev script in package.json is wrong
- D. Your Node.js version is too old

**Answer:** B. node_modules is not in the repo, so nothing is installed yet

node_modules is in .gitignore, as it should be, so a fresh clone has no packages. npm run dev looks for vite in node_modules/.bin and finds nothing. Run npm install (or npm ci) first. Installing Vite globally would hide the problem and give you a different version than the project expects.

> **Explanation:** You will run into this one every time you clone a JavaScript project. The habit: clone, check .nvmrc, install, then run.

<!-- slide 18 -->

### Task: A project that someone else can install

Homework, in a new folder that is a git repo. Stuck? The slides of chapters 1 and 2, the fnm README and the npm docs are behind the book icon.

1. Install fnm and Node.js 24, and write an .nvmrc in your project
2. npm init -y, then install prettier as a devDependency
3. Add a script "format": "prettier --write ." and a .gitignore with node_modules
4. Commit, push, clone the repo into a second folder and run npm install there
5. Done when npm run format works in the clone and git status never showed node_modules

> **Explanation:** If fnm is not found after installing it, the shell setup line is missing or your terminal was opened before you added it. For the format script, the Prettier docs have an install page with exactly these steps. git status is your check: if it lists node_modules, your .gitignore is not in the root of the repo or has a typo.

<!-- slide 19 -->

## 3. Versions and the lock file

> **Explanation:** This is the chapter where most 'it works on my laptop' problems come from. Two people with the same package.json can have different code in node_modules. The lock file is what prevents that.

<!-- slide 20 -->

### ^ and ~

A version is **major.minor.patch**, like 8.3.4. A new major may break your code, a minor adds features, a patch only fixes bugs. That is [semantic versioning](https://docs.npmjs.com/about-semantic-versioning).

| In package.json | Allows | Read as |
| --- | --- | --- |
| `^8.3.0` | 8.3.0 up to, not including, 9.0.0 | New minors and patches, same major |
| `~8.3.0` | 8.3.0 up to, not including, 8.4.0 | Only patches |
| `8.3.0` | Only 8.3.0 | Exact |
| `^0.2.3` | 0.2.3 up to, not including, 0.3.0 | Before 1.0, ^ is careful |

Each package in `package.json` has a range, not a version. Which versions match a range, you check in the [npm semver calculator](https://semver.npmjs.com/).

> **Explanation:** Semver is a promise from the maintainer, not a guarantee: some packages break things in a minor release by mistake. Before 1.0.0 every minor may break things, which is why ^ is more careful there. npm install writes ^ by default. With save-exact=true in an .npmrc file in your project, it writes exact versions instead. Some teams prefer that, because then every update is a deliberate change in package.json. The npm semver calculator shows which versions of a real package match a range you type.

<!-- slide 21 -->

### package-lock.json

*package-lock.json (one entry)*

```json
"node_modules/canvas-confetti": {
  "version": "1.9.4",
  "resolved": "https://registry.npmjs.org/
    canvas-confetti/-/canvas-confetti-1.9.4.tgz",
  "integrity": "sha512-yxQbJkAVrFXWNbTU...",
  "license": "ISC"
}
```

`package.json` says `^1.9.4`. The lock file says: it was 1.9.4, from this url, with this checksum. For every package in the tree.

**Commit it.** Without it, a classmate who installs next month can get other versions than you.

> **Explanation:** The integrity field is a checksum of the package. If the file npm downloads does not match it, the install stops. The lock file also lists every package deep in the tree, not only the ones you asked for. Merge conflicts in package-lock.json happen when two branches both install something: resolve package.json by hand, then run npm install and npm regenerates the lock file.

<!-- slide 22 -->

### npm install or npm ci

|   | `npm install` | `npm ci` |
| --- | --- | --- |
| Reads | package.json, and the lock file when it matches | Only the lock file |
| Lock file | Updates it when needed | Never touches it |
| package.json and lock disagree | Fixes the lock file | Stops with an error |
| node_modules | Adds and removes what changed | Deletes it first |
| Use it | When you add or change packages | Fresh clone, CI, a build server |

More in the [npm ci docs](https://docs.npmjs.com/cli/commands/npm-ci).

> **Explanation:** Never delete package-lock.json to 'fix' an install. It feels like a reset, but you throw away the versions that worked and get whatever is newest inside every range. If you want newer versions, update on purpose, see the next slide. The GitHub Pages workflow in the Vite docs runs npm ci, and so do most CI setups.

<!-- slide 23 -->

### Keeping up to date

```console
$ npm outdated
Package  Current  Wanted  Latest
lodash   4.17.20  4.18.1  4.18.1
vite       7.0.0   7.3.7   8.3.4
$ npm update
$ npm install vite@latest
```

Wanted is the newest version inside your range, Latest the newest there is. `npm update` takes everything to Wanted. A new major, like Vite 7 to 8, only comes with an explicit `npm install vite@latest`.

> **Explanation:** npm update stays inside the ranges of package.json, so it never jumps a major. For a major you install the new version explicitly and read the migration guide of the package first. Update one package at a time, run your project in between, and commit per update: if something breaks, you know which update did it.

<!-- slide 24 -->

### package.json says canvas-confetti ^1.9.4, the lock file says 1.9.4. Versions 1.10.0 and 2.0.0 exist. What does npm ci install on a fresh clone?

- A. 1.9.4
- B. 1.10.0
- C. 2.0.0
- D. It depends on the day

**Answer:** A. 1.9.4

npm ci installs what the lock file says, nothing else. 1.10.0 is what npm update would pick, because it is the newest version inside ^1.9.4. 2.0.0 is outside the range, so npm only installs it when you ask for it. And 'it depends on the day' is what you get without a lock file.

> **Explanation:** This is exactly why a project that worked in June still installs the same in October, as long as everyone uses the lock file.

<!-- slide 25 -->

## 4. Scripts and npx

> **Explanation:** Scripts are the part of package.json you will touch most. Every frontend project you clone starts with the same question: which script do I run? The answer is always in package.json.

<!-- slide 26 -->

### npm scripts

*package.json*

```json
"scripts": {
  "dev": "vite",
  "build": "vite build",
  "preview": "vite preview",
  "format": "prettier --write src"
}
```

1. `npm run dev` starts the dev server.
2. `npm run build` makes the production version in `dist`.
3. `npm run preview` serves that `dist` folder.
4. Your own: `npm run format` formats everything in `src`.

A script is a name for a command. `npm run` looks for that command in `node_modules/.bin`, where your terminal does not look: that is why `npm run dev` works and typing `vite` yourself does not. See the [npm run docs](https://docs.npmjs.com/cli/commands/npm-run).

> **Explanation:** This is also why you never install Vite or Prettier globally: every project carries its own version in node_modules, and two projects can use two different versions side by side. npm run without a name lists every script of the project, handy in a repo you do not know. start and test are special: npm start and npm test work without run. Scripts also run before and after each other by name: a prebuild script runs before build, without you asking. Use it sparingly, it surprises people who read your package.json.

<!-- slide 27 -->

### npx

```console
# installed in this project: runs node_modules/.bin/prettier
$ npx prettier --check src
# not installed: downloads it to a cache first, then runs it
$ npx create-vite@latest my-site
# the same, written the way the Vite docs do
$ npm create vite@latest my-site
```

> [!WARNING]
> npx downloads code and runs it right away, with your permissions. A typo in the name runs a package of somebody else. Copy the command from the docs.

> **Explanation:** When the package is not installed, npx asks 'Need to install the following packages ... Ok to proceed?' first. Read that question: it is your last chance to catch a typo in the name. npm create vite@latest is a short way of writing npx create-vite@latest: npm create adds the create- in front. The @latest makes sure you do not get an old version from the cache.

<!-- slide 28 -->

### Not only npm

|   | Add a package | Lock file | Why people pick it |
| --- | --- | --- | --- |
| [pnpm](https://pnpm.io) | `pnpm add gsap` | `pnpm-lock.yaml` | One store on your disk, fast, strict about install scripts |
| [Yarn](https://yarnpkg.com) | `yarn add gsap` | `yarn.lock` | Older alternative, still in many repos |
| [Bun](https://bun.sh) | `bun add gsap` | `bun.lock` | A whole runtime instead of Node, very fast installs |

All three read the same `package.json` and use the same registry. Stick with npm until a project or a team picks something else. The lock file in the repo tells you which one it is.

> **Explanation:** The commands are close enough that switching is easy, the lock files are not compatible: keep one package manager per project, and never commit two lock files. If a repo has a pnpm-lock.yaml, use pnpm there: install it with npm install -g pnpm or with the installer on pnpm.io.

<!-- slide 29 -->

## 5. Choosing a package

> **Explanation:** There are millions of packages on npm, and most of them you should not install. A package is code you did not write, by someone you do not know, that you will have to update for as long as the project lives. Pick with that in mind.

<!-- slide 30 -->

### Before you install

| Check | Where | Worry when |
| --- | --- | --- |
| Weekly downloads | npmjs.com or npmx.dev | A handful, and the name looks like a popular one |
| Last release, open issues | The GitHub repo | Years without a release, issues nobody answers |
| Size in the browser | [bundlephobia.com](https://bundlephobia.com) | 100 kB for something you could write in ten lines |
| Dependencies | [npmx.dev](https://npmx.dev) | Dozens of them for a small job |
| Licence | [choosealicense.com](https://choosealicense.com/licenses/) | GPL in closed work for a client, or no licence at all |

> **Explanation:** npm view gsap license prints the licence of a package. MIT, ISC, BSD and Apache-2.0 are fine anywhere, also in paid work, as long as the licence text stays with the code. GPL can force your own code to become open source, and a package without a licence is not free to use at all. No single number decides. A package with few downloads but one careful maintainer can be better than a popular one that has not been touched in four years. Install size (everything npm downloads) and bundle size (what ends up in the browser) are different: Vite is big on your disk and adds nothing to your site. npmx.dev is a newer, community-built browser for the registry: swap npmjs.com for npmx.dev in a package url.

<!-- slide 31 -->

### Maybe you do not need one

```js
// Instead of moment or date-fns for a simple date
const date = new Date('2026-10-14')
const format = new Intl.DateTimeFormat('en-GB', {
  dateStyle: 'full',
})
console.log(format.format(date))

// Instead of lodash.cloneDeep
const original = { name: 'Ada', tags: ['css'] }
const copy = structuredClone(original)
copy.tags.push('js')
console.log(original.tags, copy.tags)
```

Change the locale to `nl-BE` or `fr-BE`, and predict what the second log prints before you run it.

> **Explanation:** moment, uuid, lodash and axios are still installed millions of times a week, often in projects that do not need them anymore. The browser grew up: Intl formats dates and numbers in every language, structuredClone copies nested objects, fetch replaced most of what axios did. Before you install, search MDN for what you want to do. That is not a rule against packages: a good date picker or a 3D library is worth it.

<!-- slide 32 -->

## 6. Security

> **Explanation:** This chapter is the one to read twice. Every package you install can run code on your laptop, read your .env files and talk to the internet. 2025 was the year that went wrong at scale, for packages with billions of downloads.

<!-- slide 33 -->

### npm audit

```console
$ npm audit
lodash  <=4.17.23
Severity: high
Command Injection in lodash - https://github.com/advisories/GHSA-35jh-r3h4-6jhm
...
fix available via `npm audit fix`
$ npm audit fix
found 0 vulnerabilities
```

npm also runs the audit after every install. Read what it found and where, then fix. Details in the [npm audit docs](https://docs.npmjs.com/cli/commands/npm-audit).

> **Explanation:** npm audit compares your lock file with the GitHub Advisory Database. Many reports are about the dev server or a build tool and cannot reach your visitors, but you read them before you decide that. npm audit fix stays inside your ranges and is safe to run. npm audit fix --force also jumps majors and can break your project: never run it blindly, update the package yourself instead.

<!-- slide 34 -->

### Typosquatting

#### cross-env

The real package. Sets environment variables in a script, on every operating system. Millions of downloads a week.

#### crossenv

A fake from 2017. Did the same, and on install also sent your environment variables, tokens included, to the attacker. [npm blog](https://blog.npmjs.org/post/163723642530/crossenv-malware-on-the-npm-registry)

> **Explanation:** crossenv was not a one-off: the same account published about forty of them, like mongose and nodesass. Others hang on to the names that AI assistants invent: a model suggests a package that does not exist, someone registers that name with malware in it. So also check a package name that a chatbot gave you, before you install it.

<!-- slide 35 -->

### Install scripts

*package.json of some package*

```json
"scripts": {
  "build": "tsc",
  "postinstall": "node setup.js"
}
```

A `postinstall` script runs the moment you type `npm install`, before you import anything. npm 12 no longer runs them for dependencies until you approve a package:

```console
$ npm install-scripts ls
$ npm install-scripts approve esbuild
```

> **Explanation:** esbuild, sharp and a few other packages have a real reason for an install script: they download a binary for your system. Approve those on purpose. Node.js 24 and 26 still come with npm 11, which runs the scripts: check with npm -v. npm 12 came out in July 2026, and npm install -g npm@latest gets it for you today. pnpm has blocked install scripts of dependencies by default since version 10.

<!-- slide 36 -->

### It happened in 2025

- **[nx, August](https://www.wiz.io/blog/s1ngularity-supply-chain-attack)**: Malicious versions with a postinstall script. It searched for .env files, SSH keys and tokens, and put them in a public repo on the victim's own GitHub account.
- **[chalk and debug, September](https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised)**: A maintainer fell for a phishing mail from npmjs.help. 18 packages, over 2 billion downloads a week, with code that swapped crypto wallet addresses in the browser.
- **[Shai-Hulud, September](https://github.blog/security/supply-chain-security/our-plan-for-a-more-secure-npm-supply-chain/)**: A worm. It stole npm tokens and used them to publish itself into the packages of its victims. Over 500 packages.

> **Explanation:** All three are documented by the security companies and GitHub, the links are on the cards. The nx malware also started AI coding tools like Claude Code and Gemini CLI on the victim's laptop with their 'skip all permissions' flags, to search for secrets: one more reason to read the deck about working with AI. After Shai-Hulud, GitHub tightened publishing on npm: shorter-lived tokens, two-factor authentication and trusted publishing from CI.

<!-- slide 37 -->

### Habits that help

- Commit the lock file and install with npm ci, so nothing changes without you seeing it
- Copy package names from the docs, never type them from memory or from a chatbot
- Read what npm audit says and what an install script wants, before you approve it
- Update on purpose and not on the day of a release: min-release-age=7 in .npmrc waits a week
- Keep secrets out of reach: no live keys in a project you install random packages into
- Fewer packages, fewer strangers

**[Accounts and secrets](https://slides.mctantwerp.be/en/general/07-accounts-and-secrets)**: Two-factor authentication, a password manager and where secrets belong.

> **Explanation:** No single habit stops everything, together they shrink the window. A malicious version is usually found and removed within hours or days, so waiting a week before you take a new release avoids most of them: npm 11.10 and later have min-release-age for that, in an .npmrc file. Turn on two-factor authentication on your npm and GitHub accounts, even if you never publish.

<!-- slide 38 -->

### You install a package with a typo in its name. When can its code first run on your laptop?

- A. When you import it in your code
- B. When you run npm run build
- C. During npm install, through an install script
- D. Never, npm checks every package

**Answer:** C. During npm install, through an install script

A postinstall script runs during npm install itself, before you wrote a single import. With npm 11 that happens without asking, npm 12 blocks it until you approve the package. Nobody reviews packages before they are published: npm removes malware once it is reported, which can take hours.

> **Explanation:** The import is not the dangerous moment, the install is. That is why the name check and the approval of scripts come before npm install, not after.

<!-- slide 39 -->

## 7. Vite

> **Explanation:** With npm you can install packages. But a script tag in the browser cannot load them from node_modules. Vite is the piece in between: a dev server while you work, and a build tool that turns your project into files any web host can serve.

<!-- slide 40 -->

### The browser knows urls

*main.js*

```js
import confetti from 'canvas-confetti'

document.querySelector('button').addEventListener('click', () => confetti())
```

*Console in Chrome*

```bash
Uncaught TypeError: Failed to resolve module specifier "canvas-confetti".
Relative references must start with either "/", "./", or "../".
```

The package is in `node_modules`, but the browser has no idea where that is. Something has to turn `'canvas-confetti'` into a url.

> **Explanation:** The browser only resolves urls: a full url, or a path that starts with /, ./ or ../. A bare name like canvas-confetti means nothing to it. In the past you solved that with a CDN url in a script tag. That works, but then every visitor downloads from a third party, and you lose the lock file, the versions and the audit.

<!-- slide 41 -->

### What Vite does

#### npm run dev

- A local server for your project on localhost:5173
- Turns imports of packages into urls the browser understands
- Updates the page the moment you save a file

#### npm run build

- Bundles your code and its packages into a few files
- Minifies them and puts a hash in every file name
- Writes it all to `dist`, ready for any web host

> **Explanation:** Vite is French for 'fast', and is pronounced 'veet'. It uses native ES modules in development, so it does not have to bundle your whole project before the browser gets the first file: that is why it starts in a fraction of a second. For the production build Vite 8 uses Rolldown, a bundler written in Rust. The 'why Vite' page explains the history, back to the bundlers that came before.

<!-- slide 42 -->

### npm create vite@latest

```console
$ npm create vite@latest my-site -- --template vanilla
◇  Scaffolding project in ~/Projects/my-site...
└  Done. Now run:
  cd my-site
  npm install
  npm run dev
$ cd my-site
$ npm install
added 15 packages, and audited 16 packages in 1s
```

`vanilla` is HTML, CSS and JavaScript without a framework: start there. `react`, `vue`, `svelte` and a few more do the same with a framework, each also as `-ts` for TypeScript. All templates on [vite.dev](https://vite.dev/guide/).

> **Explanation:** Take vanilla for your first Vite project: you learn what Vite does without a framework in between. Without the flags, create-vite asks you the same questions in the terminal: a project name, a framework, a variant (JavaScript or TypeScript), and whether to install and start right away. The -- before --template passes the flag on to create-vite instead of to npm. Vite 8 needs Node.js 20.19 or 22.12 or newer, Node.js 24 is fine.

<!-- slide 43 -->

### What you get

| File or folder | What it is |
| --- | --- |
| `index.html` | The entry point, in the root and not in a folder |
| `src/main.js` | Your JavaScript, loaded from index.html |
| `src/style.css`, `src/counter.js` | CSS and a module of your own, imported in main.js |
| `src/assets/` | Images you import in your code |
| `public/` | Files copied as they are, like favicon.svg |
| `package.json`, `.gitignore` | Scripts, the vite devDependency, and node_modules and dist kept out of git |

> **Explanation:** The vanilla template has no vite.config.js: Vite works without one. You add it the moment you need a plugin or a setting. The other templates, like react, come with one because they need a plugin. The counter.js file is just an example of a module of your own, you can delete it.

<!-- slide 44 -->

### index.html is the entry

*index.html*

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <title>my-site</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
```

Vite starts from `index.html` and follows every import from there. One script tag, the rest comes in through `import`.

> **Explanation:** type=module makes the script an ES module: import and export work, the script runs after the HTML is parsed (like defer), and it has its own scope instead of the global one. The path starts with /, which Vite reads as the root of your project. In the build Vite replaces that line with the url of the bundled, hashed file.

<!-- slide 45 -->

### npm run dev

```console
$ npm run dev

  VITE v8.3.4  ready in 96 ms

  ➜  Local:   http://localhost:5173/
 ➜  Network: use --host to expose
```

Open the Local url in your browser. This server only exists on your laptop, while the terminal runs, and it is not how the site will run online.

Save a CSS file and the open page updates without a reload: that is **Hot Module Replacement**. A change in your JavaScript reloads the page. See the [features page](https://vite.dev/guide/features).

> **Explanation:** Network: use --host to expose means the server only listens on your own laptop. npm run dev -- --host also opens it to your network, so you can open the url it prints on your phone, on the same wifi. Leave the terminal open while you work: closing it, or Ctrl+C, stops the server. When 5173 is taken, Vite picks the next free port and tells you which. With a framework plugin, HMR also keeps the state of your components: a half filled in form stays filled in while you change its styling. If the page does not update at all, look at the terminal: a syntax error stops the update and shows up there and in an overlay on the page.

<!-- slide 46 -->

## 8. Importing things

> **Explanation:** Everything in a Vite project enters through import: packages, your own files, CSS and images. Once you get that, the folder structure of any modern frontend project makes sense.

<!-- slide 47 -->

### Importing a package

```console
$ npm install canvas-confetti
```

*src/main.js*

```js
import confetti from 'canvas-confetti'

const button = document.querySelector('#party')
button.addEventListener('click', party)

function party() {
  confetti({ particleCount: 150, spread: 70 })
}
```

1. Only the package name. Vite finds it in `node_modules`.
2. Plain DOM code, as you know it from year 2.
3. The options come from the README of the package.

> **Explanation:** Default export versus named export: confetti is the default export of the package, so you can call it whatever you want. Named exports are imported with curly braces and their exact name, like import { gsap } from 'gsap'. The README of a package shows which form it uses. Check the import in the README before you guess.

<!-- slide 48 -->

### Your own modules

*src/counter.js*

```js
export function setupCounter(element) {
  let count = 0
  const update = () => {
    element.textContent = `count is ${count}`
  }
  element.addEventListener('click', () => {
    count++
    update()
  })
  update()
}
```

*src/main.js*

```js
import { setupCounter } from './counter.js'

setupCounter(document.querySelector('#counter'))
```

`export` in one file, `import` with a path that starts with `./` in the other. One file per job, instead of one `script.js` of 800 lines.

> **Explanation:** With import and export every file has its own scope: a variable in counter.js does not end up global, unless you export it. That is the end of conflicts between scripts that both declare a variable called count. In your own files you write the extension: './counter.js', not './counter'. Vite forgives it, the browser and Node.js do not.

<!-- slide 49 -->

### CSS and images

*src/main.js*

```js
import './style.css'
import heroImg from './assets/hero.png'

const img = document.createElement('img')
img.src = heroImg
document.body.append(img)
```

1. Vite adds the CSS to the page. No `<link>` tag needed.
2. An image import gives you its url as a string.
3. In the build that url becomes `/assets/hero-CLDdwZDr.png`.

The hash in the name changes when the file changes. A browser can cache it forever, and still gets the new version after a deploy. See [static asset handling](https://vite.dev/guide/assets).

> **Explanation:** An imported image smaller than 4 kB is inlined as a data url in the build, so it costs no extra request. The same goes for url() in your CSS: Vite resolves, hashes and copies those images too. You can also import a CSS file from node_modules, like a stylesheet that comes with a slider package: import 'swiper/css'.

<!-- slide 50 -->

### public or src/assets

#### `src/assets/`

```js
import logo from './assets/logo.svg'
```

Imported in your code. Gets a hash, can be inlined, and a typo in the path breaks the build instead of your site.

#### `public/`

```html
<link rel="icon" href="/favicon.svg">
```

Copied to the root of dist as it is. Same name, same url. For files others link to: favicon, robots.txt, your CV.

> **Explanation:** The Vite docs prefer imports, and so do I: a missing image in src/assets is an error during the build, a missing file in public is a 404 you only find online. Use public for files that need a fixed name and url: robots.txt, a favicon, an og image that other sites link to, a PDF of your CV. Never write /public/ in a path, the folder name is not part of the url.

<!-- slide 51 -->

## 9. Build and deploy

> **Explanation:** The dev server is for you. Your visitors get something else: the folder Vite builds. This chapter is about that folder, and about the settings that make the difference between a site that works online and a white page.

<!-- slide 52 -->

### npm run build

```console
$ npm run build
vite v8.3.4 building client environment for production...
✓ 9 modules transformed.
dist/index.html                  0.45 kB │ gzip: 0.29 kB
dist/assets/hero-CLDdwZDr.png   13.05 kB
dist/assets/index-CsUDhMuy.css   4.10 kB │ gzip: 1.46 kB
dist/assets/index-CAoPt-vL.js    4.05 kB │ gzip: 1.77 kB
✓ built in 260ms
```

Everything your site needs is now in `dist`: one HTML file, one CSS file, one JavaScript file and the assets. That folder is what goes online. See [building for production](https://vite.dev/guide/build).

> **Explanation:** Rolldown is the bundler inside Vite 8. The gzip column is what actually travels over the network, because web hosts compress text files. Images are not gzipped, they are compressed already. dist is in .gitignore: you build it again on every deploy, on your laptop or, better, on the host.

<!-- slide 53 -->

### npm run preview

```console
$ npm run preview
  ➜  Local:   http://localhost:4173/
 ➜  Network: use --host to expose
```

`preview` serves the `dist` folder on port 4173, the way a web host would. Run it before every deploy: what works in dev can still break in the build, like a path to an image with a typo.

> **Explanation:** Double-clicking dist/index.html does not work: the browser opens it over file://, where module scripts are blocked and /assets/... points at the root of your disk. preview starts a small server for dist instead. It is a check, not hosting: for that you upload dist to a web host, or let a platform build it from your repo.

<!-- slide 54 -->

### Environment variables

*.env*

```bash
VITE_API_URL=https://api.example.com
DB_PASSWORD=hunter2
```

*src/main.js*

```js
import.meta.env.VITE_API_URL
// 'https://api.example.com'

import.meta.env.DB_PASSWORD
// undefined
```

Vite only hands variables that start with `VITE_` to your code. See [env variables and modes](https://vite.dev/guide/env-and-mode).

> [!WARNING]
> **VITE\_ ends up in the browser**
>
> Vite writes every `VITE_` value into the JavaScript in `dist`, readable for anyone. Never put a secret behind it: a key that has to stay secret needs a server.

> **Explanation:** Variables are always strings: VITE_MAX_ITEMS=5 gives '5', convert it yourself with Number(). After changing an .env file, restart the dev server, it only reads them on start. The Vite .gitignore only ignores *.local, so a plain .env gets committed: put values for your own laptop in .env.local. import.meta.env.DEV and PROD tell you whether you run in the dev server or the build, handy to log only while developing.

<!-- slide 55 -->

### VITE_STRIPE_SECRET_KEY is in .env.local, and .env.local is in .gitignore. Is the key safe?

- A. Yes, it never ends up in git
- B. Yes, .local files are only read on your laptop
- C. No, it ends up in the JavaScript of the build
- D. No, but only in development

**Answer:** C. No, it ends up in the JavaScript of the build

The VITE\_ prefix tells Vite to write the value into your code. .gitignore keeps the file out of your repo, but the moment you build, the key is in dist/assets for every visitor. A secret Stripe key belongs on a server: the frontend only ever gets the publishable key.

> **Explanation:** git and the bundle are two different ways to leak. .gitignore protects against the first, only a server protects against the second.

<!-- slide 56 -->

### vite.config.js

*vite.config.js*

```js
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [tailwindcss()],
  base: '/my-site/',
  server: {
    port: 3000,
  },
})
```

1. `defineConfig` gives autocomplete. The plugin is a devDependency.
2. Plugins add features, like Tailwind or React.
3. The path your site lives under online. Next slide.
4. Another port for the dev server, when 5173 does not suit you.

Create it in the root, next to `package.json`, the moment you need it. Every option on [vite.dev/config](https://vite.dev/config/).

**[Tailwind CSS](https://slides.mctantwerp.be/en/extra-webdev/03-tailwind)**: Tailwind through @tailwindcss/vite, from your first class to your own theme.

> **Explanation:** defineConfig does nothing at runtime, it gives VS Code the types so you get autocomplete on every option. A plugin is a package you install with npm install -D and add to the plugins array: @vitejs/plugin-react for React, @tailwindcss/vite for Tailwind. Every option is on the config page of vite.dev.

<!-- slide 57 -->

### GitHub Pages and base

| Your site lives at | base in vite.config.js |
| --- | --- |
| https://you.github.io/my-site/ | `base: '/my-site/'` |
| https://you.github.io/ or your own domain | Leave it out (`'/'`) |

Vite writes `/assets/index-CAoPt-vL.js` into your HTML. On a project page of GitHub Pages that points at `you.github.io/assets/`, which does not exist: a white page, and 404s in the Network tab. With `base` every url gets `/my-site/` in front. See [deploying a static site](https://vite.dev/guide/static-deploy).

**[Deploy your site](https://slides.mctantwerp.be/en/extra-webdev/04-deploy)**: Every host step by step, and your own domain.

> **Explanation:** A user site (you.github.io) or a site on its own domain lives at the root, then base stays '/' and you leave it out. The Vite docs have a ready made GitHub Actions workflow that builds and publishes dist on every push to main: Settings, Pages, Source: GitHub Actions. Netlify and Vercel find the build command and dist on their own for a Vite project.

<!-- slide 58 -->

### Task: Your first Vite project

Homework. Stuck? Chapters 7 to 9, and every slide links to the Vite docs.

1. npm create vite@latest with the vanilla template, an .nvmrc, and a first commit
2. Install a package from npm and use it, for example canvas-confetti on a button
3. An image in src/assets that you import, and a robots.txt in public
4. Show VITE_SITE_NAME from an .env file in the page
5. Done when npm run build has no errors, npm run preview shows all of it, and the repo has no node_modules or dist

> **Explanation:** Use the vanilla template, a framework would only get in the way here. For the button, the canvas-confetti README on GitHub has the options. If the image works in dev but not in preview, you probably wrote its path as a string instead of importing it. If the env variable is undefined, check the prefix and restart the dev server.

<!-- slide 59 -->

## 10. When it breaks

> **Explanation:** Everyone who builds things gets errors, all day. The difference between stuck for an hour and stuck for a minute is reading the message, from the top, instead of looking at the red and panicking.

<!-- slide 60 -->

### Read from the top

```console
[vite] Internal server error: Failed to resolve import "canvas-confetti" from "src/x.js". Does the file exist?
  Plugin: vite:import-analysis
  File: ~/Projects/my-site/src/x.js:1:22
 1  |  import confetti from 'canvas-confetti'
    |                        ^
     at TransformPluginContext._formatLog (node_modules/vite/dist/...)
     at async loadAndTransform (node_modules/vite/dist/...)
```

The first line says what went wrong, `File` says where: line 1, column 22. Everything under `at` is the inside of Vite. Here the fix is one command: `npm install canvas-confetti`.

> **Explanation:** The lines below 'at ...' are the stack trace: where inside Vite the error was thrown. Useful for the people who build Vite, rarely for you. When you paste an error in a search engine or an AI chat, paste the first lines and the file, not only the stack trace. And leave out your own paths and usernames if you post it in public.

<!-- slide 61 -->

### npm errors

| You see | It means | Fix |
| --- | --- | --- |
| `sh: vite: command not found` | No node_modules | `npm install` |
| `Missing script: "dve"` | A typo, or a script this project does not have | `npm run` lists them |
| `npm error code EACCES` | npm may not write to a global folder | fnm, never sudo |
| `npm error code ERESOLVE` | Two packages want a different version of a third | Read which, update that one |
| `npm error code E404` | That package does not exist | Check the name, twice |

> **Explanation:** On Windows the first one reads 'vite' is not recognized as an internal or external command. EACCES errors on npm install -g come from a Node.js installed with the official installer on macOS: do not reach for sudo, switch to fnm, which keeps everything in your home folder. ERESOLVE is about peer dependencies: two packages want different versions of a third. Read which ones, and update the older package instead of reaching for --force. When nothing helps: check node -v against .nvmrc, delete node_modules and run npm ci. Keep package-lock.json, deleting it installs a different project.

<!-- slide 62 -->

### Vite errors

| You see | It means | Fix |
| --- | --- | --- |
| `Failed to resolve import "x"` | Not installed, or a typo in the path | Install it, or check the `./` |
| `Port 5173 is in use` | Another dev server still runs | Use the url it prints, or stop the other one |
| `import.meta.env.X is undefined` | No VITE\_ prefix, or not restarted | Rename, restart the dev server |
| A white page after deploy | base is wrong, the JS gives a 404 | Set base, build again |
| Odd errors right after install | Node.js too old for this Vite | `node -v` and your .nvmrc |

More cases in the [Vite troubleshooting guide](https://vite.dev/guide/troubleshooting).

> **Explanation:** If you see 'Port 5173 is in use, trying another one', there is a dev server still running, often in a terminal tab you forgot. Two of them at once is how you end up editing one project and looking at the other. Vite 8 needs Node.js 20.19+ or 22.12+: on an older Node you get errors that mention neither Vite nor the version, so check node -v early.

<!-- slide 63 -->

### Your Vite site works with npm run preview. On https://you.github.io/portfolio/ you get a white page and a 404 for /assets/index-CAoPt-vL.js. What is wrong?

- A. The build failed, run npm run build again
- B. base is not set to /portfolio/
- C. GitHub Pages does not support JavaScript modules
- D. node_modules is missing on GitHub

**Answer:** B. base is not set to /portfolio/

The browser looks for the JavaScript at you.github.io/assets/, but your site lives under /portfolio/. With base: '/portfolio/' in vite.config.js every url in the build gets that prefix. The build itself was fine, GitHub Pages serves modules without a problem, and node_modules is never needed online: dist holds everything.

> **Explanation:** Everything works in npm run dev, and the build is fine: the problem is in where the files end up online. The Network tab shows it in one look.

<!-- slide 64 -->

### Task: Break it on purpose

Homework, on a branch of the project from the previous assignment. Stuck? The two error tables and the slide Read from the top.

1. Delete node_modules and run npm run dev. Note the error, then fix it.
2. Import a package you did not install, and use a VITE\_ variable without the prefix
3. Set base to /wrong/ and check npm run preview, the Network tab included
4. Write per error the first line of the message and your fix in a TROUBLESHOOTING.md
5. Done when your notes cover at least four errors and the project builds again

> **Explanation:** Breaking things on purpose is the fastest way to recognise them later, when you did not mean to. Work on a branch, so you can throw it away afterwards. If an error message differs from the table, write down the one you got: your versions may be newer than the ones in this deck.

## Glossary

- **Node.js**: A program that runs JavaScript outside the browser, on your laptop or a server. npm, Vite and most frontend tools are written in JavaScript and need it. (slide 5)
- **LTS**: Long Term Support: a Node.js line that gets fixes for about two and a half years. Use the LTS for your projects, not the Current line. (slide 7)
- **Version manager**: A tool like fnm or nvm that installs several Node.js versions side by side and switches between them per project. (slide 8)
- **.nvmrc**: A file in the root of a project with the Node.js version it needs, like 24. fnm and nvm read it and switch to that version. (slide 9)
- **npm**: The package manager that comes with Node.js, and the name of the registry at npmjs.com where the packages live. (slide 11)
- **package.json**: The file that describes your project: its name, its scripts and the packages it needs, with a version range per package. (slide 13)
- **devDependency**: A package you only need while building or checking your code, like Vite or Prettier. Installed with npm install -D. (slide 14)
- **node_modules**: The folder where npm puts every installed package and their dependencies. Never in git: npm install builds it again from package.json and the lock file. (slide 16)
- **Semantic versioning**: Version numbers as major.minor.patch. A new major may break your code, a minor adds features, a patch only fixes bugs. (slide 20)
- **Caret range**: A version range with ^, like ^8.3.0: every version from 8.3.0 up to, but not including, 9.0.0. What npm install writes by default. (slide 20)
- **package-lock.json**: The file npm writes with the exact version and checksum of every package in node_modules. Commit it, so everyone installs the same tree. (slide 21)
- **npm ci**: Installs exactly what the lock file says, after deleting node_modules. Fails when package.json and the lock file disagree. For CI and a fresh clone. (slide 22)
- **npx**: Runs the command of a package: from node_modules/.bin when it is installed, otherwise downloaded to a cache first. (slide 27)
- **Typosquatting**: Publishing a malicious package under a name that looks like a popular one, like crossenv for cross-env, and waiting for a typo. (slide 34)
- **Install script**: A preinstall, install or postinstall script of a package that runs on npm install, with your permissions. npm 12 blocks them for dependencies until you approve them. (slide 35)
- **Supply chain attack**: An attack through the code you depend on: a hijacked maintainer account publishes a malicious version, and everybody who installs it runs it. (slide 36)
- **Bare import**: An import with only a package name, like import confetti from "canvas-confetti". The browser cannot resolve it, a tool like Vite can. (slide 40)
- **Vite**: A dev server and build tool for the web. In development it serves your files as ES modules with HMR, for production it bundles them into dist. (slide 41)
- **HMR**: Hot Module Replacement: the dev server swaps the module you saved in the open page, without a full reload. CSS updates without losing what is on screen. (slide 45)
- **base**: The Vite option with the path your site lives under. For https://you.github.io/repo/ that is /repo/, or every url to your JavaScript and CSS breaks. (slide 57)

## Links and sources

### In the deck

- [Introduction to Node.js](https://nodejs.org/en/learn/getting-started/introduction-to-nodejs) (slide 5)
- [Node.js releases](https://nodejs.org/en/about/previous-releases) (slide 7): Current and LTS lines, and the yearly schedule from Node.js 27
- [fnm](https://github.com/Schniz/fnm) (slide 8): Install, shell setup, .nvmrc and .node-version
- [npmjs.com](https://www.npmjs.com) (slide 11)
- [npm init](https://docs.npmjs.com/cli/commands/npm-init) (slide 11): npm create is an alias, npm create vite runs create-vite
- [About semantic versioning](https://docs.npmjs.com/about-semantic-versioning) (slide 20)
- [npm semver calculator](https://semver.npmjs.com/) (slide 20)
- [npm ci](https://docs.npmjs.com/cli/commands/npm-ci) (slide 22)
- [npm run](https://docs.npmjs.com/cli/commands/npm-run) (slide 26)
- [pnpm](https://pnpm.io) (slide 28)
- [Yarn](https://yarnpkg.com) (slide 28)
- [Bun](https://bun.sh) (slide 28)
- [Bundlephobia](https://bundlephobia.com) (slide 30)
- [npmx](https://npmx.dev) (slide 30): A browser for the npm registry, with install size and module format
- [Choose a license](https://choosealicense.com/licenses/) (slide 30)
- [npm audit](https://docs.npmjs.com/cli/commands/npm-audit) (slide 33)
- [crossenv malware on the npm registry](https://blog.npmjs.org/post/163723642530/crossenv-malware-on-the-npm-registry) (slide 34): Typosquatting, 2017
- [s1ngularity: supply chain attack leaks secrets on GitHub](https://www.wiz.io/blog/s1ngularity-supply-chain-attack) (slide 36): The nx packages, August 2025
- [npm debug and chalk packages compromised](https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised) (slide 36): September 2025
- [Our plan for a more secure npm supply chain](https://github.blog/security/supply-chain-security/our-plan-for-a-more-secure-npm-supply-chain/) (slide 36): Shai-Hulud, September 2025
- [Vite: getting started](https://vite.dev/guide/) (slide 42): npm create vite, templates, Node 20.19+ or 22.12+
- [Vite: features](https://vite.dev/guide/features) (slide 45): Bare imports, CSS, HMR
- [Vite: static asset handling](https://vite.dev/guide/assets) (slide 49): Imported assets and the public directory
- [Vite: building for production](https://vite.dev/guide/build) (slide 52)
- [Vite: env variables and modes](https://vite.dev/guide/env-and-mode) (slide 54)
- [Vite: configuring Vite](https://vite.dev/config/) (slide 56)
- [Vite: deploying a static site](https://vite.dev/guide/static-deploy) (slide 57): vite preview and GitHub Pages
- [Vite: troubleshooting](https://vite.dev/guide/troubleshooting) (slide 62)

### Sources

- [Node.js release schedule](https://github.com/nodejs/Release): Node 24 Active LTS until 2026-10-20, Node 26 LTS from 2026-10-28
- [Download Node.js](https://nodejs.org/en/download)
- [nvm](https://github.com/nvm-sh/nvm)
- [nvm for Windows](https://github.com/coreybutler/nvm-windows)
- [npm install](https://docs.npmjs.com/cli/commands/npm-install)
- [package.json](https://docs.npmjs.com/cli/configuring-npm/package-json)
- [package-lock.json](https://docs.npmjs.com/cli/configuring-npm/package-lock-json)
- [npm scripts](https://docs.npmjs.com/cli/using-npm/scripts): Lifecycle scripts like postinstall
- [npx](https://docs.npmjs.com/cli/commands/npx)
- [npm outdated](https://docs.npmjs.com/cli/commands/npm-outdated)
- [npm install-scripts](https://docs.npmjs.com/cli/commands/npm-install-scripts): Approving install scripts of dependencies in npm 12
- [npm v12.0.0 release notes](https://github.com/npm/cli/releases/tag/v12.0.0): Dependency lifecycle scripts blocked by default
- [Vite: why Vite](https://vite.dev/guide/why)
- [Tailwind CSS with Vite](https://tailwindcss.com/docs/installation/using-vite)
- [JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules): MDN
