Lesson 1 of 6
Move the tracker onto Vite and record a baseline
Scaffold a Vite + TypeScript project from your Module 1 HTML/CSS/JS tracker, write observable acceptance criteria, and record what actually happens before any agent change.
Same app behaviour, new toolchain
Download the complete Module 2 starter ZIP. No earlier purchase is required. Unzip into a new ~/projects/task-tracker-safe folder. It includes every implementation, test helper, config, and the verify-only CI workflow used in these lessons. Use current Node 22 (22.22.2 or newer) to satisfy the bundled test tools.
cd ~/projects/task-tracker-safe
npm install
npm run typecheck
npm test
npm run dev
Open http://127.0.0.1:4173; expect Task tracker, not the Vite logo. Stop the server with Ctrl+C before browser tests. This reference app starts with passing checks: your work is to understand them, demonstrate a deliberate red, add your own criterion, and verify a bounded change. Never present bundled tests as tests you authored.
Module 1 shipped a working Task Tracker as three static files under public/: index.html, styles.css, and app.js. You served them with python3 -m http.server or npx serve. This module keeps that user-visible behaviour and moves the same app onto Vite + TypeScript so you can type-check and test it. Module 1 did not teach Vite; that starts here.
Do not copy the Module 1 files byte-for-byte. In Vite, index.html lives at the project root (not inside public/) and loads /src/main.ts as an ES module. npm run dev replaces the old static server. npm run build writes a dist/ folder.
Storage is still browser localStorage only. Module 1 saved { id, text } under task-tracker.tasks.v1. The typed app uses a versioned envelope under lintlabs-task-tracker-v1. Those keys are different, so old tasks will not appear after the move. That is expected. A public URL still shares files, not another person's list.
Scaffold Vite + TypeScript and pin the port
Optional from-scratch route — skip the scaffold if you downloaded the starter. Use current Node 22 (22.22.2 or newer). Scaffold vanilla TypeScript in a different new folder so neither Module 1 nor the downloaded reference is overwritten:
cd ~/projects
npm create vite@latest task-tracker-safe-manual -- --template vanilla-ts
cd task-tracker-safe-manual
npm install
Run npm run dev once and confirm the Vite starter loads (default port is 5173). Stop it. Pin one URL so you and later browser tests agree. Create vite.config.ts:
import { defineConfig } from 'vite';
export default defineConfig({
server: { host: '127.0.0.1', port: 4173, strictPort: true },
preview: { host: '127.0.0.1', port: 4173, strictPort: true }
});
Run npm run dev again and open http://127.0.0.1:4173. You should see the Vite starter, not the Module 1 tracker yet. Record the title you actually saw. If 4173 is busy, stop the other process; strictPort will not silently pick another port.
Port add, status, and reload — then inspect storage
Starter route: inspect src/model.ts, src/storage.ts, src/app.ts, and src/main.ts; all the following behavior is already implemented. Manual route: copy those files from the ZIP before continuing, or implement the same contract and compare. The full storage envelope is { version: 1, tasks: [{ id, title, status, createdAt }] }, not a bare array.
Replace the Vite demo with tracker behaviour. Keep index.html at the root with <main id="app"> and <script type="module" src="/src/main.ts"></script>. Put rules in src/model.ts with no DOM:
export const TITLE_MAX_LENGTH = 160;
export function validateTitle(value: unknown): string {
const title = String(value ?? '').trim();
if (!title) throw new Error('Please describe a task before adding it.');
if (title.length > TITLE_MAX_LENGTH) throw new Error('Use 160 characters or fewer.');
return title;
}
In src/main.ts, mount a labelled Task input, an Add task button, a role="status" line, and a list. Save JSON to localStorage key lintlabs-task-tracker-v1. On a valid add, clear the input and set the status to Task added.
Run npm run dev, add Buy milk, reload the tab, and confirm the row is still there. In DevTools → Application → Local Storage, open that key. You should see JSON in this browser only. Clearing site data deletes the list. That is the product limit, not a missing server.
Turn wishes into observable acceptance criteria
An agent will invent what “nicer” means unless you write a contract. Each criterion names setup, action, and observable result. “Saving should work” is not a criterion. This is: given an empty tracker, when I submit Buy milk, then one row shows that title, the input is empty, and status text says it was added.
Create acceptance-criteria.md with these rows, plus two failure paths:
- Add a valid task (newest first; input clears).
- Reject empty or whitespace input; nothing is saved.
- Reject titles over 160 characters; accept exactly 160.
- Reload in the same browser still shows the task.
- Toggle done; the title is unchanged.
- Filter Open/Done without deleting stored tasks.
- Storage unavailable: show an error; never claim success.
- Unreadable stored JSON: show the problem and leave the stored bytes untouched.
For each row, name the check that proves it, or write not covered. Do not invent a test filename you will not create.
Run what exists and fill the baseline column
A baseline is what the app and its checks do now, before an agent edits anything. The ZIP already declares and configures these tools. Run npm install there; only the manual scaffold needs these additions:
npm install -D vitest jsdom
Add scripts "typecheck": "tsc --noEmit" and "test": "vitest run". Create vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: { include: ['tests/**/*.test.ts'], environment: 'jsdom', restoreMocks: true }
});
Run npm run typecheck and npm test. Copy the exact output. If there are no tests/**/*.test.ts files yet, Vitest reporting zero tests is a baseline of not covered, not a pass. Fill a Baseline column with pass, fail, or not covered, plus the command and a short excerpt. Leave that column alone later. Clicking through the UI is a useful note; it is not a named check until a test file exists.