chore: Add script to show missing envs * base * Better output * Add missing flag * Add script to check env Approved-by: Joakim Jäderberg
217 lines
6.4 KiB
JavaScript
217 lines
6.4 KiB
JavaScript
#!/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)
|
|
})
|