|
| 1 | +--- |
| 2 | +slug: holy-shit-this-escalated-quickly |
| 3 | +title: |
| 4 | + Holy Shit, This Escalated Quickly - Axogen v0.5.0 Is Here And I Can't Even |
| 5 | +authors: [oliverseifert] |
| 6 | +--- |
| 7 | + |
| 8 | +Originally I wanted to write this article at the end of the week, but it just |
| 9 | +happened too much in the meantime. |
| 10 | + |
| 11 | +Since my last article about 1.5 weeks went past. I was on holidays for 5 days so |
| 12 | +not much progress then, but in the other time we got this shit that made me |
| 13 | +realize: maybe I actually built something people want? |
| 14 | + |
| 15 | +**TL;DR: My .env rant got featured in DEV's Top 7, I got so hyped I locked |
| 16 | +myself in my house for 4 days straight and shipped secret detection, 10+ file |
| 17 | +formats, nested commands, backup systems, and proper type safety. We're at |
| 18 | +v0.5.0 now and honestly I can't believe this is working.** |
| 19 | + |
| 20 | +<!-- truncate --> |
| 21 | + |
| 22 | +## Wait, Dev.to Featured Me in Their Top 7?! |
| 23 | + |
| 24 | +So first things first - I GOT PROMOTED ON THE DEV.TO WEEKLY TOP 7. Like how |
| 25 | +freaking crazy is that!!!!! |
| 26 | + |
| 27 | +[Top 7 Featured DEV Posts of the Week](https://dev.to/devteam/top-7-featured-dev-posts-of-the-week-1490) |
| 28 | + |
| 29 | +This happened 2 days ago and I'm still processing it. My little config tool rant |
| 30 | +sitting next to articles with thousands of views. We're at about 400 reads total |
| 31 | +now - not massive, but people are reading about my .env file frustrations and |
| 32 | +thinking "yeah, this guy gets it." |
| 33 | + |
| 34 | +While my article was doing its thing, I was so nervous that I couldn't stop |
| 35 | +myself from coding. I haven't left my house in 4 days... So I did what any |
| 36 | +reasonable developer would do: I went completely overboard with new features and |
| 37 | +improvements. |
| 38 | + |
| 39 | +## The Type Safety Revolution (Finally!) |
| 40 | + |
| 41 | +Remember how the old API was kinda... loose? Yeah, I fixed that. |
| 42 | + |
| 43 | +The new recommended way uses dedicated functions for each target type, and |
| 44 | +TypeScript now actually knows what you're doing: |
| 45 | + |
| 46 | +```typescript |
| 47 | +import {defineConfig, env, json} from "@axonotes/axogen"; |
| 48 | + |
| 49 | +export default defineConfig({ |
| 50 | + targets: { |
| 51 | + someService: json({ |
| 52 | + path: "output/someService.json", |
| 53 | + variables: { |
| 54 | + name: "MyService", |
| 55 | + version: "1.3.0", |
| 56 | + }, |
| 57 | + }), |
| 58 | + myEnvironment: env({ |
| 59 | + path: "output/.env", |
| 60 | + variables: { |
| 61 | + PRODUCTION: "true", |
| 62 | + API_KEY: "12345", |
| 63 | + DATABASE_URL: "postgres://user:pass@localhost:5432/mydb", |
| 64 | + }, |
| 65 | + }), |
| 66 | + }, |
| 67 | +}); |
| 68 | +``` |
| 69 | + |
| 70 | +You still can use the old structure, but now you get proper IntelliSense for |
| 71 | +each target type. |
| 72 | + |
| 73 | +## Secret Detection That Prevents Disasters |
| 74 | + |
| 75 | +Here's something that kept me up at night: what if someone accidentally pushes |
| 76 | +their production API keys because Axogen generated them into a non-gitignored |
| 77 | +file? |
| 78 | + |
| 79 | +So I built secret detection. Before generation of each target, all variables are |
| 80 | +scanned. If they look like secrets (API keys, passwords, tokens, etc.) and the |
| 81 | +target file is NOT gitignored, Axogen refuses to generate. |
| 82 | + |
| 83 | + |
| 84 | + |
| 85 | +> Also, did you notice 😏, this time I used a screenshot to show off the colored |
| 86 | +> output? I know, I know, I'm a rebel. |
| 87 | +
|
| 88 | +But sometimes you actually want to generate "secrets" - like development |
| 89 | +database URLs or test API keys. Just wrap them with `unsafe()`: |
| 90 | + |
| 91 | +```typescript |
| 92 | +import {defineConfig, env, unsafe} from "@axonotes/axogen"; |
| 93 | + |
| 94 | +export default defineConfig({ |
| 95 | + targets: { |
| 96 | + myEnvironment: env({ |
| 97 | + path: "output/.env", |
| 98 | + variables: { |
| 99 | + PRODUCTION: false, |
| 100 | + API_KEY: unsafe("your-dev-api-key-here", "Development mode"), |
| 101 | + }, |
| 102 | + }), |
| 103 | + }, |
| 104 | +}); |
| 105 | +``` |
| 106 | + |
| 107 | +The second parameter is required - you have to explicitly say WHY this is safe. |
| 108 | +No more "oops, I pushed the prod keys" moments. |
| 109 | + |
| 110 | +## Zod Schema Validation (Because Type Safety Everywhere) |
| 111 | + |
| 112 | +Remember how I said this was powered by Zod but you couldn't really use Zod's |
| 113 | +full power? Fixed that too. |
| 114 | + |
| 115 | +Now you can validate your target configurations before generating, and |
| 116 | +TypeScript will complain if your variables don't match your schema: |
| 117 | + |
| 118 | +```typescript |
| 119 | +import {defineConfig, json} from "@axonotes/axogen"; |
| 120 | +import * as z from "zod"; |
| 121 | + |
| 122 | +export default defineConfig({ |
| 123 | + targets: { |
| 124 | + someService: json({ |
| 125 | + path: "output/someService.json", |
| 126 | + schema: z.object({ |
| 127 | + name: z.string().describe("The name of the service"), |
| 128 | + version: z.string().describe("The version of the service"), |
| 129 | + }), |
| 130 | + variables: { |
| 131 | + name: "MyService", |
| 132 | + version: 2, // TSC will error: Type 'number' is not assignable to type 'string' |
| 133 | + }, |
| 134 | + }), |
| 135 | + }, |
| 136 | +}); |
| 137 | +``` |
| 138 | + |
| 139 | +Your editor lights up with red squiggles before you even try to generate. And if |
| 140 | +you somehow ignore TypeScript (why would you do that?), Zod catches it at |
| 141 | +runtime with beautiful error messages. |
| 142 | + |
| 143 | +## Backup System (I have trust-issue okay?) |
| 144 | + |
| 145 | +You know that moment when you run a generation command and accidentally |
| 146 | +overwrite something important? Yeah, me too. |
| 147 | + |
| 148 | +Axogen can now create backups of your targets before overwriting them: |
| 149 | + |
| 150 | +```typescript |
| 151 | +import {defineConfig, json} from "@axonotes/axogen"; |
| 152 | + |
| 153 | +export default defineConfig({ |
| 154 | + targets: { |
| 155 | + someService: json({ |
| 156 | + path: "output/someService.json", |
| 157 | + variables: { |
| 158 | + name: "MyService", |
| 159 | + version: "1.3.0", |
| 160 | + }, |
| 161 | + backup: true, |
| 162 | + // backupPath: "your/own/backup/path.json", // Optional |
| 163 | + }), |
| 164 | + }, |
| 165 | +}); |
| 166 | +``` |
| 167 | + |
| 168 | +Default backup location is `.axogen/backup/{{path}}`. Currently keeps one |
| 169 | +backup - though I'm thinking about adding more sophisticated backup options with |
| 170 | +retention policies. |
| 171 | + |
| 172 | +## Conditional Generation |
| 173 | + |
| 174 | +Sometimes you only want to generate certain configs under certain conditions. |
| 175 | +Now you can: |
| 176 | + |
| 177 | +```typescript |
| 178 | +export default defineConfig({ |
| 179 | + targets: { |
| 180 | + productionSecrets: env({ |
| 181 | + path: "secrets/.env.prod", |
| 182 | + variables: { |
| 183 | + REAL_API_KEY: "super-secret-key", |
| 184 | + PROD_DATABASE_URL: "postgres://prod-server/db", |
| 185 | + }, |
| 186 | + condition: process.env.NODE_ENV === "production", |
| 187 | + }), |
| 188 | + }, |
| 189 | +}); |
| 190 | +``` |
| 191 | + |
| 192 | +The `condition` is just a boolean - use it for whatever logic you want. |
| 193 | +Environment checks, feature flags, time-based generation, whatever makes sense |
| 194 | +for your use case. |
| 195 | + |
| 196 | +## Command System Overhaul (Nested Commands Are Here) |
| 197 | + |
| 198 | +The old command system was... functional. The new one is beautiful. |
| 199 | + |
| 200 | +You can now create as many nested command groups as you want, and they're all |
| 201 | +auto-registered with full type safety. There are actually 5 different ways to |
| 202 | +define commands: |
| 203 | + |
| 204 | +```typescript |
| 205 | +import {cmd, defineConfig, group, liveExec} from "@axonotes/axogen"; |
| 206 | +import * as z from "zod"; |
| 207 | + |
| 208 | +export default defineConfig({ |
| 209 | + commands: { |
| 210 | + hello: "echo 'Hello, world!'", // Simple string |
| 211 | + build: async (context) => { |
| 212 | + // Direct function |
| 213 | + console.log("Building..."); |
| 214 | + await liveExec("bun run build"); |
| 215 | + }, |
| 216 | + dev: cmd({ |
| 217 | + help: "Give a string command some help text", |
| 218 | + command: "echo 'This is a dev command'", |
| 219 | + }), |
| 220 | + echo: cmd({ |
| 221 | + help: "A command that echoes the input", |
| 222 | + args: { |
| 223 | + input: z.string().describe("The input to echo"), |
| 224 | + }, |
| 225 | + exec: (context) => { |
| 226 | + console.log(context.args.input); |
| 227 | + }, |
| 228 | + }), |
| 229 | + database: group({ |
| 230 | + help: "Database management commands", |
| 231 | + commands: { |
| 232 | + migrate: cmd({ |
| 233 | + help: "Run database migrations", |
| 234 | + exec: (context) => console.log("Running migrations..."), |
| 235 | + }), |
| 236 | + seed: cmd({ |
| 237 | + help: "Seed the database with test data", |
| 238 | + exec: (context) => console.log("Seeding database..."), |
| 239 | + }), |
| 240 | + backup: group({ |
| 241 | + help: "Database backup operations", |
| 242 | + commands: { |
| 243 | + create: cmd({ |
| 244 | + help: "Create a database backup", |
| 245 | + options: { |
| 246 | + name: z.string().describe("Backup name"), |
| 247 | + }, |
| 248 | + exec: (ctx) => { |
| 249 | + console.log( |
| 250 | + `Creating backup: ${ctx.options.name}` |
| 251 | + ); |
| 252 | + }, |
| 253 | + }), |
| 254 | + }, |
| 255 | + }), |
| 256 | + }, |
| 257 | + }), |
| 258 | + }, |
| 259 | +}); |
| 260 | +``` |
| 261 | + |
| 262 | +Now `axogen run database backup create --name "before-migration"` just works. |
| 263 | +The help system is automatically generated. IntelliSense knows about all your |
| 264 | +args and options. It's like having a professional CLI framework built into your |
| 265 | +config tool. |
| 266 | + |
| 267 | +## Environment Loading Revolution |
| 268 | + |
| 269 | +The `loadEnv` function got a complete makeover. You can now configure the |
| 270 | +loading process and select different files: |
| 271 | + |
| 272 | +```typescript |
| 273 | +import {loadEnv} from "@axonotes/axogen"; |
| 274 | +import * as z from "zod"; |
| 275 | + |
| 276 | +const env = loadEnv( |
| 277 | + z.object({ |
| 278 | + NODE_ENV: z.enum(["development", "production"]).default("development"), |
| 279 | + PORT: z.coerce.number().default(3000), |
| 280 | + DATABASE_URL: z.url().describe("The URL of the database"), |
| 281 | + }), |
| 282 | + { |
| 283 | + path: ".env.custom", |
| 284 | + // Any dotenvx options work here |
| 285 | + verbose: true, |
| 286 | + override: true, |
| 287 | + } |
| 288 | +); |
| 289 | +``` |
| 290 | + |
| 291 | +Full dotenvx compatibility means you get all the advanced environment loading |
| 292 | +features, but with Zod validation on top. |
| 293 | + |
| 294 | +## Universal File Loading |
| 295 | + |
| 296 | +But wait, there's more! Ever wanted to load TOML, YAML, or other config formats |
| 297 | +with the same type safety? |
| 298 | + |
| 299 | +```typescript |
| 300 | +import {loadFile} from "@axonotes/axogen"; |
| 301 | +import * as z from "zod"; |
| 302 | + |
| 303 | +const config = loadFile( |
| 304 | + "myFile.toml", |
| 305 | + "toml", |
| 306 | + z.object({ |
| 307 | + key1: z.string().describe("Description for key1"), |
| 308 | + key2: z.number().describe("Description for key2"), |
| 309 | + key3: z.boolean().describe("Description for key3"), |
| 310 | + }) |
| 311 | +); |
| 312 | +``` |
| 313 | + |
| 314 | +Works with basically any config format you throw at it. Same Zod validation, |
| 315 | +same type safety, same beautiful error messages. |
| 316 | + |
| 317 | +## File Format Explosion |
| 318 | + |
| 319 | +Speaking of formats, I went a bit crazy with the supported file types: |
| 320 | + |
| 321 | +**For loading:** |
| 322 | + |
| 323 | +- json, json5, jsonc, hjson |
| 324 | +- yaml |
| 325 | +- toml |
| 326 | +- ini, properties |
| 327 | +- env |
| 328 | +- xml, csv, cson |
| 329 | + |
| 330 | +**For generating:** |
| 331 | + |
| 332 | +- All the loading formats, plus... |
| 333 | +- template (nunjucks, handlebars, mustache) |
| 334 | + |
| 335 | +## Performance & Code Quality (The Boring But Important Stuff) |
| 336 | + |
| 337 | +Version 0.5.0 isn't just about features. I spent a lot of time on: |
| 338 | + |
| 339 | +- **Better error handling**: Clearer messages, better stack traces |
| 340 | +- **Performance improvements**: Mainly TSC cleanup - wasn't slow before, just a |
| 341 | + tiny bit faster now |
| 342 | +- **Code structure**: Much cleaner internals, easier to contribute to |
| 343 | +- **Safety improvements**: More validation, fewer edge cases |
| 344 | + |
| 345 | +The kind of stuff you don't notice until you don't have to think about it. |
| 346 | + |
| 347 | +## Join the Discord (Why Not?) |
| 348 | + |
| 349 | +Oh, and I haven't mentioned this before, but we already had a Discord server for |
| 350 | +Axonotes: |
| 351 | + |
| 352 | +[Join the Axonotes Discord](https://discord.gg/myBMaaDeQu) |
| 353 | + |
| 354 | +Come ask questions, complain about bugs, or just chat about config management. |
| 355 | +People can join and ask Axogen questions there too if they want. |
| 356 | + |
| 357 | +## What's Next? |
| 358 | + |
| 359 | +We're at v0.5.0 now, and honestly? I'm just getting started. Some big things on |
| 360 | +the roadmap: |
| 361 | + |
| 362 | +- **Project initialization**: `npx @axonotes/axogen init` command |
| 363 | +- **Secrets management**: Doppler, Vault, AWS Secrets Manager integration |
| 364 | +- **Runtime loading**: Import your generated configs directly into your app |
| 365 | + |
| 366 | +But right now, I'm just excited that this thing I built in frustration is |
| 367 | +helping some people. The few GitHub stars (about 5 now), the Discord |
| 368 | +conversations - it's all reminding me why I love building tools. |
| 369 | + |
| 370 | +With that in mind, Axogen is still not production ready. But I'm working on it! |
| 371 | +I still have 1.5 weeks of spare time to spend lol. |
| 372 | + |
| 373 | +## Try The New Hotness |
| 374 | + |
| 375 | +If you tried the old version and bounced off, give v0.5.0 a shot. If you haven't |
| 376 | +tried it yet, now's a great time: |
| 377 | + |
| 378 | +```bash |
| 379 | +npm install @axonotes/axogen@latest |
| 380 | + |
| 381 | +# Check out the updated docs for examples |
| 382 | +# (not updated yet XD, maybe just use this article as a guide): |
| 383 | +# https://axonotes.github.io/axogen/ |
| 384 | +``` |
| 385 | + |
| 386 | +The API is much more stable now, the type safety is useful, and the features |
| 387 | +solve real problems instead of just being neat ideas. |
| 388 | + |
| 389 | +Configuration management doesn't have to suck. Your environment variables can |
| 390 | +have types. Your scripts can be intelligent. Your deployments can be consistent. |
| 391 | + |
| 392 | +Still feels surreal, honestly. |
| 393 | + |
| 394 | +--- |
| 395 | + |
| 396 | +_Built with ❤️, excessive caffeine, and the validation of internet strangers by |
| 397 | +[Oliver Seifert](https://github.com/imgajeed76). Now at v0.5.0 - still proceed |
| 398 | +with reasonable caution, but maybe a little less caution than before._ |
0 commit comments