From 1f41398370cd753c7e27c9bc598b3f4e3fec5239 Mon Sep 17 00:00:00 2001 From: Amritanshu Date: Tue, 22 Sep 2026 21:47:11 +0530 Subject: [PATCH] Make dev mode one command and add VS Code config npm run dev now runs scripts/dev-env.ts first, which creates .dev/site.env with a known dev password and a random session secret, so a fresh clone serves a working admin without anyone hand-building a hash. Adds .vscode launch configurations and tasks for the dev server, one test file, and check-site, and brings scripts/ under typecheck, which it had been missing. --- .dockerignore | 1 + .vscode/launch.json | 58 +++++++++++++++++++++++++++++++++++++++++++ .vscode/settings.json | 23 +++++++++++++++++ .vscode/tasks.json | 49 ++++++++++++++++++++++++++++++++++++ README.md | 48 +++++++++++++++++++++++++++-------- package.json | 2 ++ scripts/dev-env.ts | 57 ++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 2 +- 8 files changed, 229 insertions(+), 11 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 scripts/dev-env.ts diff --git a/.dockerignore b/.dockerignore index ff65d41..4d0418f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,7 @@ node_modules .tmp .dev .git +.vscode public/fonts/OFL.txt docs test diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a02cd3d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,58 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Dev server (debug)", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/src/server.ts", + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "skipFiles": ["/**"], + "preLaunchTask": "dev: ensure .dev/site.env", + "env": { + "MENU_ROOT": "${workspaceFolder}/.dev", + "MENU_SITE_CONFIG": "${workspaceFolder}/config/hopsngrains.site.json", + "SECURE_COOKIES": "false", + "LOG_LEVEL": "info", + "PORT": "3000" + } + }, + { + "name": "Dev server (watch, no debugger)", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev", + "cwd": "${workspaceFolder}" + }, + { + "name": "Debug the open test file", + "type": "node", + "request": "launch", + "program": "${file}", + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "skipFiles": ["/**"], + "runtimeArgs": ["--test"], + "autoAttachChildProcesses": true + }, + { + "name": "Check the running site", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/scripts/check-site.ts", + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "skipFiles": ["/**"], + "args": ["${input:siteUrl}"] + } + ], + "inputs": [ + { + "id": "siteUrl", + "type": "promptString", + "description": "Base URL to check", + "default": "http://127.0.0.1:3000" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3ecef2d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "files.exclude": { + "**/.dev": true, + "**/.tmp": true, + "**/.npm-cache": true + }, + "search.exclude": { + "**/.dev": true, + "**/.tmp": true, + "**/.npm-cache": true, + "**/node_modules": true + }, + "files.watcherExclude": { + "**/.dev/**": true, + "**/.tmp/**": true, + "**/.npm-cache/**": true + }, + "editor.tabSize": 2, + "editor.insertSpaces": true, + "files.eol": "\n", + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..b0382d8 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,49 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "dev: ensure .dev/site.env", + "type": "npm", + "script": "dev:env", + "problemMatcher": [] + }, + { + "label": "dev: seed placeholder menus", + "type": "npm", + "script": "seed", + "problemMatcher": [] + }, + { + "label": "dev: watch server", + "type": "npm", + "script": "dev", + "problemMatcher": [] + }, + { + "label": "test", + "type": "npm", + "script": "test", + "group": { "kind": "test", "isDefault": true }, + "problemMatcher": [] + }, + { + "label": "typecheck", + "type": "npm", + "script": "typecheck", + "group": { "kind": "build", "isDefault": true }, + "problemMatcher": ["$tsc"] + }, + { + "label": "verify the data directory", + "type": "npm", + "script": "verify", + "problemMatcher": [] + }, + { + "label": "repair missing derivatives", + "type": "npm", + "script": "repair", + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index 4579ce4..7dc2ecd 100644 --- a/README.md +++ b/README.md @@ -54,27 +54,55 @@ deploy.sh Tests, build, push The Ansible playbook lives in `ansible-playbooks/menu-admin/`, beside the other playbooks, because that is where it is run from. -## Working on it +## Running it locally -Node 24 or newer runs the TypeScript sources directly. +Node 24 or newer runs the TypeScript sources directly; there is no build step. ```sh npm install -npm test # node:test -npm run typecheck # tsc --noEmit -npm run seed # placeholder Pages in .dev/data, for design review +npm run seed # placeholder Pages in .dev (wipes .dev/data; for looking at the design) npm run dev # http://localhost:3000 against .dev ``` -To sign in locally, put a password hash in `.dev/site.env`: +`npm run dev` first runs `scripts/dev-env.ts`, which creates `.dev/site.env` on +first use with the password `menu-dev-password` (override with +`DEV_ADMIN_PASSWORD=...`) and a random session secret, then starts the server +with `--watch`. Sign in at . + +The file is gitignored and development only: the dev server runs with +`SECURE_COOKIES=false` over http, and that password is a known constant. On the +server the same file is written by `scripts/provision-site-env.sh` with a hash of +the real password and mode 600. + +Tests and checks: ```sh -printf 'ADMIN_PASSWORD_HASH=%s\nSESSION_SECRET=%s\n' \ - "$(node -e "import('./src/auth.ts').then(async m => process.stdout.write(await m.hashPassword('your password')))")" \ - "$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64url'))")" \ - > .dev/site.env +npm test # node:test +npm run typecheck # tsc --noEmit, scripts/ included +npm run verify # integrity of .dev/data +npm run check-site -- http://127.0.0.1:3000 # checks a running site from outside ``` +### In VS Code + +`.vscode/` holds launch configurations and tasks. + +| Configuration | What it does | +| --- | --- | +| **Dev server (debug)** | runs `src/server.ts` with the dev environment, breakpoints live | +| **Dev server (watch, no debugger)** | `npm run dev` in a terminal | +| **Debug the open test file** | runs the file you have open under `node --test`, with child processes attached so breakpoints inside a test bind | +| **Check the running site** | prompts for a URL and runs `scripts/check-site.ts` | + +Breakpoints land in the `.ts` files themselves: Node strips types in place, so +line numbers are the ones you see. The debug configuration creates `.dev/site.env` +first (`preLaunchTask`), so a fresh clone serves a working admin immediately. + +Tasks (Command Palette → *Run Task*) cover the same ground: seed, the dev server, +tests, typecheck, verify and repair. `.dev`, `.tmp` and `.npm-cache` are hidden +from the explorer, search and the file watcher, so a watched server does not +restart on its own data writes. + ## Who can do what `/` and `/
/` are public. `/admin` needs the password; a Draft's Pages diff --git a/package.json b/package.json index 1cd1312..d87d11d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ }, "scripts": { "start": "node src/server.ts", + "predev": "node scripts/dev-env.ts", "dev": "SECURE_COOKIES=false node --watch src/server.ts", + "dev:env": "node scripts/dev-env.ts", "seed": "node scripts/seed-dev.ts", "check-site": "node scripts/check-site.ts", "verify": "node src/cli/verify.ts", diff --git a/scripts/dev-env.ts b/scripts/dev-env.ts new file mode 100644 index 0000000..5773d10 --- /dev/null +++ b/scripts/dev-env.ts @@ -0,0 +1,57 @@ +/** + * Prepares a local development Site so `npm run dev` has a working admin + * without anyone hand-building a password hash. + * + * Development only: the file it writes is gitignored, holds a fixed well-known + * password, and the dev server runs with SECURE_COOKIES=false over http. + */ + +import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'; +import { randomBytes } from 'node:crypto'; +import path from 'node:path'; +import { hashPassword } from '../src/auth.ts'; + +const DEV_PASSWORD = process.env['DEV_ADMIN_PASSWORD'] ?? 'menu-dev-password'; +const root = path.resolve(process.env['MENU_ROOT'] ?? '.dev'); +const envFile = path.join(root, 'site.env'); + +async function exists(target: string): Promise { + try { + await readFile(target); + return true; + } catch { + return false; + } +} + +async function hasContent(dir: string): Promise { + try { + return (await readdir(dir)).length > 0; + } catch { + return false; + } +} + +if (await exists(envFile)) { + console.log(`${envFile} already exists — using it (delete the file to reset the dev password)`); +} else { + await mkdir(root, { recursive: true }); + const hash = await hashPassword(DEV_PASSWORD); + const secret = randomBytes(32).toString('base64url'); + await writeFile( + envFile, + [ + '# Development only, created by scripts/dev-env.ts. Gitignored.', + `ADMIN_PASSWORD_HASH=${hash}`, + `SESSION_SECRET=${secret}`, + '', + ].join('\n'), + { mode: 0o600 }, + ); + console.log(`wrote ${envFile}`); + console.log(` admin password: ${DEV_PASSWORD} (override with DEV_ADMIN_PASSWORD=...)`); +} + +if (!(await hasContent(path.join(root, 'data', 'published')))) { + console.log('no published menus yet — run `npm run seed` for placeholder ones'); +} diff --git a/tsconfig.json b/tsconfig.json index 8094e81..66752bf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,5 @@ "skipLibCheck": true, "types": ["node"] }, - "include": ["src/**/*.ts", "test/**/*.ts"] + "include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts"] }