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.
This commit is contained in:
tanshu committed 2026-09-22 21:47:11 +05:30
1 parent a635c5a214
commit 1f41398370
8 files changed
+229 -11

No files matched your search

+1
View File
@@ -3,6 +3,7 @@ node_modules
.tmp .tmp
.dev .dev
.git .git
.vscode
public/fonts/OFL.txt public/fonts/OFL.txt
docs docs
test test
+58
View File
@@ -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": ["<node_internals>/**"],
"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": ["<node_internals>/**"],
"runtimeArgs": ["--test"],
"autoAttachChildProcesses": true
},
{
"name": "Check the running site",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/scripts/check-site.ts",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"],
"args": ["${input:siteUrl}"]
}
],
"inputs": [
{
"id": "siteUrl",
"type": "promptString",
"description": "Base URL to check",
"default": "http://127.0.0.1:3000"
}
]
}
+23
View File
@@ -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
}
+49
View File
@@ -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": []
}
]
}
+38 -10
View File
@@ -54,27 +54,55 @@ deploy.sh Tests, build, push
The Ansible playbook lives in `ansible-playbooks/menu-admin/`, beside the other The Ansible playbook lives in `ansible-playbooks/menu-admin/`, beside the other
playbooks, because that is where it is run from. 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 ```sh
npm install npm install
npm test # node:test npm run seed # placeholder Pages in .dev (wipes .dev/data; for looking at the design)
npm run typecheck # tsc --noEmit
npm run seed # placeholder Pages in .dev/data, for design review
npm run dev # http://localhost:3000 against .dev 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 <http://localhost:3000/admin>.
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 ```sh
printf 'ADMIN_PASSWORD_HASH=%s\nSESSION_SECRET=%s\n' \ npm test # node:test
"$(node -e "import('./src/auth.ts').then(async m => process.stdout.write(await m.hashPassword('your password')))")" \ npm run typecheck # tsc --noEmit, scripts/ included
"$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64url'))")" \ npm run verify # integrity of .dev/data
> .dev/site.env 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 ## Who can do what
`/` and `/<section>/` are public. `/admin` needs the password; a Draft's Pages `/` and `/<section>/` are public. `/admin` needs the password; a Draft's Pages
+2
View File
@@ -8,7 +8,9 @@
}, },
"scripts": { "scripts": {
"start": "node src/server.ts", "start": "node src/server.ts",
"predev": "node scripts/dev-env.ts",
"dev": "SECURE_COOKIES=false node --watch src/server.ts", "dev": "SECURE_COOKIES=false node --watch src/server.ts",
"dev:env": "node scripts/dev-env.ts",
"seed": "node scripts/seed-dev.ts", "seed": "node scripts/seed-dev.ts",
"check-site": "node scripts/check-site.ts", "check-site": "node scripts/check-site.ts",
"verify": "node src/cli/verify.ts", "verify": "node src/cli/verify.ts",
+57
View File
@@ -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<boolean> {
try {
await readFile(target);
return true;
} catch {
return false;
}
}
async function hasContent(dir: string): Promise<boolean> {
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');
}
+1 -1
View File
@@ -18,5 +18,5 @@
"skipLibCheck": true, "skipLibCheck": true,
"types": ["node"] "types": ["node"]
}, },
"include": ["src/**/*.ts", "test/**/*.ts"] "include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts"]
} }