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:
1 parent
a635c5a214
commit
1f41398370
8 files changed
+229
-11
No files matched your search
@@ -3,6 +3,7 @@ node_modules
|
||||
.tmp
|
||||
.dev
|
||||
.git
|
||||
.vscode
|
||||
public/fonts/OFL.txt
|
||||
docs
|
||||
test
|
||||
|
||||
Vendored
+58
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+23
@@ -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
|
||||
}
|
||||
Vendored
+49
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 <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
|
||||
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 `/<section>/` are public. `/admin` needs the password; a Draft's Pages
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -18,5 +18,5 @@
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
"include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user