Add ESLint and Prettier

Prettier owns formatting: .prettierrc.json, format and format:check scripts, and
a one-off pass over the repo so the check is green from the start. Embedded
language formatting is off deliberately - the renderers build HTML in html
tagged templates and letting Prettier reflow those would change the markup
customers receive. docs/ is excluded because it is hand-wrapped prose, and shell
scripts have no Prettier parser.

ESLint covers the JavaScript: the two browser scripts, the build and check
scripts, and its own config. no-var is scoped out for the browser scripts on
purpose; they are plain ES5-shaped IIFEs and a hoisting difference would only
show up in a browser.

ESLint cannot lint the TypeScript here: typescript@7 is the native port with no
JavaScript compiler API, and typescript-eslint requires a 4.8-6.1 peer, so it
refuses to install and forcing it through would leave a parser that cannot read
the compiler. tsc --noEmit stays the TypeScript gate; README records the two
ways to change that.

lint and format:check now run in deploy.sh, and VS Code formats on save with
Prettier and surfaces ESLint findings.
This commit is contained in:
tanshu committed 2026-09-22 23:15:08 +05:30
1 parent ce8079355b
commit e64f65642f
40 files changed
+1469 -172

No files matched your search

+44 -9
View File
@@ -3,7 +3,13 @@ import { rm } from 'node:fs/promises';
import path from 'node:path';
import { afterEach, beforeEach, describe, it } from 'node:test';
import type { FastifyInstance } from 'fastify';
import { LoginThrottle, SESSION_COOKIE, createSessionToken, csrfToken, hashPassword } from '../src/auth.ts';
import {
LoginThrottle,
SESSION_COOKIE,
createSessionToken,
csrfToken,
hashPassword,
} from '../src/auth.ts';
import { parseSiteConfig } from '../src/config.ts';
import { buildApp } from '../src/server.ts';
import { FakePipeline, pngBytes, tempDir, testClock } from './support/harness.ts';
@@ -237,7 +243,11 @@ describe('the editor', () => {
assert.equal(response.statusCode, 303);
assert.equal(response.headers['location'], `/admin/sets/${id}`);
const editor = await app.inject({ method: 'GET', url: `/admin/sets/${id}`, cookies: cookies() });
const editor = await app.inject({
method: 'GET',
url: `/admin/sets/${id}`,
cookies: cookies(),
});
assert.equal(editor.statusCode, 200);
assert.match(editor.body, /data-drop/);
assert.match(editor.body, /data-pages-admin/);
@@ -281,7 +291,11 @@ describe('uploading Pages', () => {
assert.equal(response.statusCode, 200);
assert.deepEqual(response.json(), { added: 1, failed: [] });
const editor = await app.inject({ method: 'GET', url: `/admin/sets/${id}`, cookies: cookies() });
const editor = await app.inject({
method: 'GET',
url: `/admin/sets/${id}`,
cookies: cookies(),
});
assert.match(editor.body, /IMG_0001\.JPG/);
assert.match(editor.body, /\/media\/thumb-/);
});
@@ -566,7 +580,11 @@ describe('forms as a browser sends them', () => {
it('publishes from the editor form, with no CSRF header', async () => {
const { id } = await newSet();
await upload(id, 'one.jpg');
const editor = await app.inject({ method: 'GET', url: `/admin/sets/${id}`, cookies: cookies() });
const editor = await app.inject({
method: 'GET',
url: `/admin/sets/${id}`,
cookies: cookies(),
});
const response = await app.inject({
method: 'POST',
url: `/admin/sets/${id}/publish`,
@@ -582,7 +600,11 @@ describe('forms as a browser sends them', () => {
await upload(id, 'one.jpg');
await upload(id, 'two.jpg');
const before = (await app.menu.store.get(id)).manifest.pages.map((page) => page.id);
const editor = await app.inject({ method: 'GET', url: `/admin/sets/${id}`, cookies: cookies() });
const editor = await app.inject({
method: 'GET',
url: `/admin/sets/${id}`,
cookies: cookies(),
});
const response = await app.inject({
method: 'POST',
url: `/admin/sets/${id}/move`,
@@ -606,7 +628,11 @@ describe('forms as a browser sends them', () => {
it('accepts the token as a multipart field, the way the no-JS upload form sends it', async () => {
const { id } = await newSet();
const editor = await app.inject({ method: 'GET', url: `/admin/sets/${id}`, cookies: cookies() });
const editor = await app.inject({
method: 'GET',
url: `/admin/sets/${id}`,
cookies: cookies(),
});
const token = csrfFrom(editor.body);
const boundary = '----formboundary';
@@ -661,9 +687,14 @@ describe('forms as a browser sends them', () => {
const { id } = await newSet('bar', 'Bar Menu');
await upload(id, 'one.jpg');
const dashboard = (await app.inject({ method: 'GET', url: '/admin', cookies: cookies() })).body;
const editor = (await app.inject({ method: 'GET', url: `/admin/sets/${id}`, cookies: cookies() })).body;
const editor = (
await app.inject({ method: 'GET', url: `/admin/sets/${id}`, cookies: cookies() })
).body;
for (const [where, body] of [['dashboard', dashboard], ['editor', editor]] as const) {
for (const [where, body] of [
['dashboard', dashboard],
['editor', editor],
] as const) {
const forms = [...body.matchAll(/<form\b[^>]*>[\s\S]*?<\/form>/g)].map((match) => match[0]);
assert.ok(forms.length > 0, `no forms found in the ${where}`);
for (const form of forms) {
@@ -691,7 +722,11 @@ describe('reordering', () => {
it('offers dragging on the Current set and the new order reaches customers', async () => {
const id = await publishBar('Bar Menu', '2026-06-26');
const editor = await app.inject({ method: 'GET', url: `/admin/sets/${id}`, cookies: cookies() });
const editor = await app.inject({
method: 'GET',
url: `/admin/sets/${id}`,
cookies: cookies(),
});
assert.equal(editor.statusCode, 200);
assert.match(editor.body, /data-pages-admin/);
assert.match(editor.body, /page-admin__handle/);