Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cd_dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ jobs:
git stash
git pull
npm install
pm2 start --node-args="--env-file-if-exists=.env" -i max bin/rerum_v1.js
pm2 startOrReload ecosystem.config.json --env development
2 changes: 1 addition & 1 deletion .github/workflows/cd_prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ jobs:
git stash
git pull
npm install
pm2 start --node-args="--env-file-if-exists=.env" -i max bin/rerum_v1.js
pm2 startOrReload ecosystem.config.json --env production
5 changes: 5 additions & 0 deletions bin/rerum_v1.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#!/usr/bin/env node

// Must be the first import: populates process.env from .env synchronously
// before any other module reads it. See env-loader.js for why this lives
// here instead of being injected via PM2 `node_args`.
import '../env-loader.js'

/**
* Module dependencies.
*/
Expand Down
22 changes: 22 additions & 0 deletions ecosystem.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "https://json.schemastore.org/pm2-ecosystem.json",
"apps": [
{
"name": "rerum_v1",
"script": "./bin/rerum_v1.js",
"instances": "max",
"exec_mode": "cluster",
"env_development": { "NODE_ENV": "development" },
"env_production": { "NODE_ENV": "production" },
"listen_timeout": 10000,
"kill_timeout": 5000,
"wait_ready": false,
"max_memory_restart": "500M",
"log_date_format": "YYYY-MM-DD HH:mm:ss Z",
"merge_logs": true,
"autorestart": true,
"max_restarts": 10,
"min_uptime": "10s"
}
]
}
81 changes: 81 additions & 0 deletions env-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Environment Variable Loader
*
* Preloads variables from a `.env` file into `process.env` before any
* application module reads them. Runs synchronously as a side-effect import.
*
* Two entry points use this loader, by design:
* 1. The app entry script `bin/rerum_v1.js` imports this file FIRST,
* before any other module. Because ES module imports evaluate in
* source order, this guarantees `process.env` is populated before
* `app.js` (or anything it pulls in) reads it. This is the path used
* under PM2 in development and production, and it is independent of
* PM2's `node_args` / cluster-worker `execArgv` plumbing — which was
* observed not to fire reliably on the RHEL servers
* (vlcdhp02 / vlcdhprdp02).
* 2. The test/coverage scripts in `package.json` pass it via
* `node --import ./env-loader.js`, because tests do not go through
* `bin/rerum_v1.js`.
*
* Replaces the previous `--env-file-if-exists=.env` Node CLI flag, which
* was unreliable on the RHEL servers under PM2.
*
* Behavior:
* - Resolves `.env` against this file's own directory (via
* `import.meta.url`), NOT `process.cwd()`. PM2 cluster workers on
* RHEL have shown inconsistent cwd handling; anchoring to the file
* location guarantees the same `.env` is found regardless of where
* the process was launched from.
* - Permissive: if `.env` is missing, logs a warning and continues
* with whatever is already in `process.env`.
* - Non-destructive: does NOT overwrite keys already set in
* `process.env`, so PM2-injected env, CI secrets, and shell exports
* still take precedence.
* - Strips a leading UTF-8 BOM (U+FEFF) if present, so `.env` files
* saved by Windows editors do not lose their first key.
* - No external dependency — uses only `node:fs`, `node:path`, and
* `node:url`.
*/

import { readFileSync, existsSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const RESET = '\x1b[0m'
const YELLOW = '\x1b[33m'
const GREEN = '\x1b[32m'
const CYAN = '\x1b[36m'

const __dirname = dirname(fileURLToPath(import.meta.url))
const envPath = resolve(__dirname, '.env')

if (!existsSync(envPath)) {
console.warn(`${YELLOW}[env-loader] .env not found at ${envPath} — continuing with existing process.env${RESET}`)
} else {
let loaded = 0
let skipped = 0
let contents = readFileSync(envPath, 'utf8')
if (contents.charCodeAt(0) === 0xFEFF) contents = contents.slice(1)
for (const raw of contents.split(/\r?\n/)) {
const line = raw.trim()
if (!line || line.startsWith('#')) continue
const eq = line.indexOf('=')
if (eq === -1) continue
const key = line.slice(0, eq).trim()
if (!key) continue
let val = line.slice(eq + 1).trim()
if (
(val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))
) {
val = val.slice(1, -1)
}
if (key in process.env) {
skipped++
continue
}
process.env[key] = val
loaded++
}
console.log(`${GREEN}[env-loader] loaded ${loaded} vars from ${envPath}${RESET}${skipped ? ` ${CYAN}(skipped ${skipped} already set)${RESET}` : ''}`)
}
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
"npm": ">=11.0.0"
},
"scripts": {
"start": "node --env-file-if-exists=.env ./bin/rerum_v1.js",
"test": "node --env-file-if-exists=.env --import ./test/bootstrap.js --test --test-skip-pattern='@e2e' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js",
"test:ci": "node --env-file-if-exists=.env --import ./test/bootstrap.js --test __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js",
"test:e2e": "node --env-file-if-exists=.env --import ./test/bootstrap.js --test --test-name-pattern='@e2e' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js",
"coverage": "c8 --reporter=html --reporter=text --include='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --env-file-if-exists=.env --import ./test/bootstrap.js --test --test-skip-pattern='@e2e' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js",
"coverage:ci": "c8 --reporter=html --reporter=json --reporter=text --include='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --env-file-if-exists=.env --import ./test/bootstrap.js --test __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js"
"start": "node --import ./env-loader.js ./bin/rerum_v1.js",
"test": "node --import ./env-loader.js --import ./test/bootstrap.js --test --test-skip-pattern='@e2e' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js",
"test:ci": "node --import ./env-loader.js --import ./test/bootstrap.js --test __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js",
"test:e2e": "node --import ./env-loader.js --import ./test/bootstrap.js --test --test-name-pattern='@e2e' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js",
"coverage": "c8 --reporter=html --reporter=text --include='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --import ./env-loader.js --import ./test/bootstrap.js --test --test-skip-pattern='@e2e' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js",
"coverage:ci": "c8 --reporter=html --reporter=json --reporter=text --include='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --import ./env-loader.js --import ./test/bootstrap.js --test __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js"
},
"dependencies": {
"cookie-parser": "~1.4.7",
Expand Down
6 changes: 5 additions & 1 deletion routes/__tests__/route_wrappers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import searchRouter from '../search.js'
import queryRouter from '../query.js'
import releaseRouter from '../release.js'
import apiRoutesRouter from '../api-routes.js'
import gogFragmentsRouter from '../_gog_fragments_from_manuscript.js'
import gogGlossesRouter from '../_gog_glosses_from_manuscript.js'

function getRoute(router, path) {
const routeLayer = router.stack.find(layer => layer.route?.path === path)
Expand Down Expand Up @@ -206,7 +208,9 @@ describe('unsupported-method 405 fallbacks', () => {
{ label: '/delete/:_id', router: deleteRouter, path: '/:_id' },
{ label: '/history/:_id', router: historyRouter, path: '/:_id' },
{ label: '/id/:_id', router: idRouter, path: '/:_id' },
{ label: '/since/:_id', router: sinceRouter, path: '/:_id' }
{ label: '/since/:_id', router: sinceRouter, path: '/:_id' },
{ label: '/_gog_fragments_from_manuscript', router: gogFragmentsRouter, path: '/' },
{ label: '/_gog_glosses_from_manuscript', router: gogGlossesRouter, path: '/' }
]

for (const { label, router, path } of cases) {
Expand Down
3 changes: 1 addition & 2 deletions routes/_gog_fragments_from_manuscript.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ router.route('/')
.post(auth.checkJwt, controller._gog_fragments_from_manuscript)
.all((req, res, next) => {
res.statusMessage = 'Improper request method. Please use POST.'
res.status(405)
next(res)
res.status(405).end()
})

export default router
3 changes: 1 addition & 2 deletions routes/_gog_glosses_from_manuscript.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ router.route('/')
.post(auth.checkJwt, controller._gog_glosses_from_manuscript)
.all((req, res, next) => {
res.statusMessage = 'Improper request method. Please use POST.'
res.status(405)
next(res)
res.status(405).end()
})

export default router
Loading