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:
216
scripts/show-env.mjs
Normal file
216
scripts/show-env.mjs
Normal 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)
|
||||
})
|
||||
Reference in New Issue
Block a user