Add ESLint, and run it with the tests in CI

A variable declared inside a try block but read after it shipped and broke
collection checks for a day; tests can't reach that path and node --check
only parses. ESLint flags it as no-undef. Wired into npm run lint / check
and into CI before the image build, so a failure stops the build.

Three findings fixed on the way: an unread argument, and two values
assigned then immediately replaced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dlawler489 2026-09-21 11:40:43 +10:00
parent 517574720e
commit ac88a247ae
8 changed files with 1008 additions and 6 deletions

View file

@ -16,6 +16,13 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Lint and test
# In the runner's own Docker, so this needs no extra actions. A failure
# here stops the image being built, rather than deploying a broken one.
run: |
docker run --rm -v "$PWD":/src -w /src node:22-bookworm-slim \
sh -c "npm ci --no-audit --no-fund && npm run lint && npm test"
- name: Log in to the Forgejo container registry
# REGISTRY_TOKEN = a Forgejo access token with write:package scope, added
# as a repo Actions secret. The auto GITHUB_TOKEN can't push packages.

View file

@ -55,7 +55,9 @@ A self-hosted web app for organizing your 3D-printing files (STL, 3MF, OBJ, FBX,
```
4. Open http://localhost:3000
Run the unit tests with `npm test`.
Run the unit tests with `npm test`, ESLint with `npm run lint`, or both with `npm run check`. CI runs both before
building the image, so a lint error stops a broken image being pushed. The lint rules are deliberately thin — their
job is the class of mistake tests can't catch, chiefly a name that doesn't exist until the line finally runs.
### Where your data lives

45
eslint.config.js Normal file
View file

@ -0,0 +1,45 @@
// Lint rules, kept to the mistakes tests can't catch — chiefly a name that
// doesn't exist, which only shows up when that line finally runs.
const js = require('@eslint/js');
const globals = require('globals');
module.exports = [
{ ignores: ['node_modules/**', 'uploads/**', 'thumbnails/**', 'glb/**', 'extracted/**', 'models/**', 'data/**', 'backups/**', 'tools/venv/**'] },
// The server: Node, CommonJS.
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2023,
sourceType: 'commonjs',
globals: { ...globals.node },
},
rules: {
...js.configs.recommended.rules,
'no-unused-vars': ['error', { argsIgnorePattern: '^_|^next$', caughtErrors: 'none' }],
},
},
// The browser: plain scripts sharing globals across files, plus one module.
{
files: ['public/**/*.js'],
languageOptions: {
sourceType: 'script',
globals: {
...globals.browser,
ModelViewer: 'readonly', // viewer.js hangs it on window
PRINTER_MODELS: 'readonly', // printer-art.js does the same
},
},
},
{
files: ['public/viewer.js'],
languageOptions: { sourceType: 'module' },
},
// Tests.
{
files: ['test/**/*.js'],
languageOptions: { globals: { ...globals.node } },
},
];

View file

@ -52,7 +52,7 @@ async function fetchHtml(url) {
}
// Extract Open Graph / title metadata from raw HTML.
function parseMetadata(html, url) {
function parseMetadata(html, _url) { // _url: kept for the call shape, not read
const titleTag = (html.match(/<title[^>]*>([^<]*)<\/title>/i) || [])[1];
return {
title: metaContent(html, 'property', 'og:title') || (titleTag ? decodeEntities(titleTag).trim() : null),

943
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,14 @@
{
"name": "printhive",
"version": "1.0.0",
"description": "Printhive — a web app for organizing 3D-printing files (STL, 3MF, OBJ, ...) with URL import",
"description": "Printhive \u2014 a web app for organizing 3D-printing files (STL, 3MF, OBJ, ...) with URL import",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "node --test test/*.test.js"
"test": "node --test test/*.test.js",
"lint": "eslint .",
"check": "npm run lint && npm test"
},
"keywords": [
"stl",
@ -35,6 +37,9 @@
"tar": "^7.5.22"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"eslint": "^10.11.0",
"globals": "^17.12.0",
"nodemon": "^3.0.2"
}
}

View file

@ -2908,7 +2908,7 @@ function printerStatusHtml(snap, model, p = {}) {
async function pollPrinters() {
clearTimeout(printersPoll);
if (selectedScope !== 'printers' || !document.getElementById('printerCards')) return;
let live = {};
let live;
try {
live = await getJson('/api/printers/live');
} catch {

View file

@ -1028,7 +1028,7 @@ async function importFromUrl(url, { projects = [], withFiles = true } = {}) {
let filesError = null; // why no files could be downloaded, shown after the import
let filesErrorReason = null; // makerworld_token: the app offers to update the token
let license = meta.license || null;
let commercial = null;
let commercial;
if (detected.site === 'makerworld' && html) {
if (withFiles) {
files = await downloadMakerworldFiles(html).catch((err) => {