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
|
.tmp
|
||||||
.dev
|
.dev
|
||||||
.git
|
.git
|
||||||
|
.vscode
|
||||||
public/fonts/OFL.txt
|
public/fonts/OFL.txt
|
||||||
docs
|
docs
|
||||||
test
|
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
|
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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
"skipLibCheck": true,
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
"include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts"]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user