Merged in chore/env-scripts (pull request #2481)

chore: Add script to show missing envs

* base

* Better output

* Add missing flag

* Add script to check env


Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2025-06-30 13:54:24 +00:00
parent 361fd75133
commit c1daef39f2
2 changed files with 219 additions and 1 deletions

216
scripts/show-env.mjs Normal file
View File

@@ -0,0 +1,216 @@
#!/usr/bin/env node
import fs from 'fs/promises'
import path from 'path'
// --- Helpers ---
async function fileExists(filePath) {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}
function getAppDir(appName) {
// Try apps/appName, then root
const candidates = [path.join(process.cwd(), 'apps', appName), path.join(process.cwd(), appName)]
return candidates.find((dir) =>
fs
.stat(dir)
.then((s) => s.isDirectory())
.catch(() => false)
)
}
async function getMonorepoPackages() {
const pkgsDir = path.join(process.cwd(), 'packages')
const entries = await fs.readdir(pkgsDir)
return entries.filter((e) => !e.startsWith('.'))
}
async function getAppDependencies(appDir) {
const pkgJsonPath = path.join(appDir, 'package.json')
const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'))
const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies }
return Object.keys(allDeps || {}).filter((dep) => dep.startsWith('@scandic-hotels/'))
}
async function findEnvFiles(baseDir) {
const envDir = path.join(baseDir, 'env')
const files = []
for (const f of ['server.ts', 'client.ts']) {
const full = path.join(envDir, f)
if (await fileExists(full)) files.push(full)
}
return files
}
async function extractEnvVarsFromFile(filePath) {
const content = await fs.readFile(filePath, 'utf8')
const regex = /process\.env\.([A-Z0-9_]+)/g
const vars = new Set()
let match
while ((match = regex.exec(content))) {
vars.add(match[1])
}
return Array.from(vars)
}
async function readEnvLocalVars(envPath) {
try {
const content = await fs.readFile(envPath, 'utf8')
const lines = content.split(/\r?\n/)
const vars = new Set()
for (const line of lines) {
const m = line.match(/^\s*([A-Z0-9_]+)\s*=/)
if (m) vars.add(m[1])
}
return vars
} catch {
return new Set()
}
}
// --- New: Extract zod schema example values ---
function extractZodExamplesFromContent(content) {
// Heuristic: find zod schema definitions in the file
// Only supports simple z.string(), z.enum([...]), z.coerce.number(), .default(), .refine()
const result = {}
// Match server: { ... } or client: { ... }
const serverMatch = content.match(/(server|client):\s*{([\s\S]*?)}\s*,/)
if (!serverMatch) return result
const block = serverMatch[2]
// Match lines like VAR: z.string().default("foo"),
const lineRegex =
/([A-Z0-9_]+):\s*z\.(string|enum|coerce\.number|coerce\.boolean|number|boolean)\(([^)]*)\)([^,]*)/g
let match
while ((match = lineRegex.exec(block))) {
const [_, varName, zodType, zodArgs, zodChain] = match
let example = 'example'
if (zodType === 'string') {
if (/refine\(.*s === \"true\" \|\| s === \"false\"/.test(zodChain)) {
example = '"true/false"'
} else if (/default\(["']([^"']+)["']\)/.test(zodChain)) {
example = '"' + zodChain.match(/default\(["']([^"']+)["']\)/)[1] + '"'
} else {
example = '"example"'
}
} else if (zodType === 'enum') {
// z.enum(["a", "b"]) => "a|b"
const opts = zodArgs
.replace(/\[|\]|\s|"|'/g, '')
.split(',')
.filter(Boolean)
example = '"' + opts.join('|') + '"'
} else if (zodType.includes('number')) {
if (/default\((\d+(\.\d+)?)\)/.test(zodChain)) {
example = '"' + zodChain.match(/default\((\d+(\.\d+)?)\)/)[1] + '"'
} else {
example = '"123"'
}
} else if (zodType.includes('boolean')) {
example = '"true/false"'
}
result[varName] = example
}
return result
}
async function extractEnvExamplesFromFile(filePath) {
const content = await fs.readFile(filePath, 'utf8')
return extractZodExamplesFromContent(content)
}
async function main() {
const args = process.argv.slice(2)
const appName = args[0]
const group = args.includes('--group')
const missing = args.includes('--missing')
if (!appName) {
console.error('Usage: node show-env.mjs <app-name> [--group] [--missing]')
process.exit(1)
}
const appDir = getAppDir(appName)
if (!appDir) {
console.error(`App directory not found for: ${appName}`)
process.exit(1)
}
const deps = await getAppDependencies(appDir)
const monorepoPkgs = await getMonorepoPackages()
const usedPkgs = deps
.map((dep) => dep.replace('@scandic-hotels/', ''))
.filter((pkg) => monorepoPkgs.includes(pkg))
// Collect env files
const sources = [{ name: appName, dir: appDir }]
for (const pkg of usedPkgs) {
sources.push({ name: pkg, dir: path.join(process.cwd(), 'packages', pkg) })
}
const envVarsBySource = {}
const envExamplesBySource = {}
for (const src of sources) {
const files = await findEnvFiles(src.dir)
const vars = new Set()
const examples = {}
for (const file of files) {
for (const v of await extractEnvVarsFromFile(file)) vars.add(v)
const ex = await extractEnvExamplesFromFile(file)
Object.assign(examples, ex)
}
envVarsBySource[src.name] = Array.from(vars).sort()
envExamplesBySource[src.name] = examples
}
function getExample(varName, source) {
return envExamplesBySource[source]?.[varName] || '"example"'
}
// If --missing, read .env.local from appDir
let definedVars = new Set()
if (missing) {
const envPath = path.join(appDir, '.env.local')
definedVars = await readEnvLocalVars(envPath)
}
function filterMissing(vars) {
if (!missing) return vars
return vars.filter((v) => !definedVars.has(v))
}
if (group) {
// Print grouped by source
for (const [src, vars] of Object.entries(envVarsBySource)) {
const missingVars = filterMissing(vars)
if (missingVars.length === 0) continue
console.log(`\n[${src}]`)
for (const v of missingVars) console.log(`${v}=${getExample(v, src)}`)
}
} else {
// Print all unique env vars, sorted, with example
const allVars = new Set()
for (const vars of Object.values(envVarsBySource)) {
for (const v of vars) allVars.add(v)
}
const sortedVars = Array.from(allVars).sort()
const missingVars = filterMissing(sortedVars)
for (const v of missingVars) {
let example = '"example"'
for (const src of Object.keys(envExamplesBySource)) {
if (envExamplesBySource[src][v]) {
example = envExamplesBySource[src][v]
break
}
}
console.log(`${v}=${example}`)
}
}
}
main().catch((err) => {
console.error(err)
process.exit(1)
})