-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp_options.go
More file actions
590 lines (559 loc) · 23.3 KB
/
Copy pathapp_options.go
File metadata and controls
590 lines (559 loc) · 23.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
package nexus
import (
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"time"
"github.com/paulmanoni/nexus/di"
"github.com/paulmanoni/nexus/httpx"
)
// Option composes a nexus app. Everything returned by Provide, Supply,
// Invoke, Module, AsRest, AsQuery, AsMutation, AsWebSocket, AsSubscription
// is an Option, ready to pass to Run. The DI container is an implementation
// detail — user code imports only nexus.
type Option interface{ nexusOption() di.Option }
// Lifecycle and Hook are re-exported from the di seam so extensions can take a
// lifecycle parameter and register start/stop hooks without importing di
// directly. The builtin container provides Lifecycle natively; the opt-in fx
// adapter bridges fx.Lifecycle onto it.
type (
Lifecycle = di.Lifecycle
Hook = di.Hook
)
type rawOption struct{ o di.Option }
func (r rawOption) nexusOption() di.Option { return r.o }
// routerOption carries a chosen HTTP router backend. It is consumed BEFORE the
// graph is built (Run scans for it and seeds Config.Router, since the router
// is constructed inside New(cfg) which runs ahead of user options). Its
// container contribution is therefore a no-op.
type routerOption struct{ r httpx.Router }
func (routerOption) nexusOption() di.Option { return di.Options() }
// containerOption carries a chosen DI backend. Like routerOption it is consumed
// before the graph is built (Run scans for it), so its own graph contribution
// is a no-op.
type containerOption struct{ backend di.Backend }
func (containerOption) nexusOption() di.Option { return di.Options() }
// WithContainer selects the dependency-injection backend (default: the
// zero-dependency builtin container in nexus/di). Pass the opt-in fx adapter to
// switch:
//
// nexus.Boot(nexus.WithContainer(fxcontainer.New()))
//
// Selecting the fx adapter pulls go.uber.org/fx (and dig) into the build; the
// builtin default links none of it. Mirrors WithRouter.
func WithContainer(backend di.Backend) Option { return containerOption{backend: backend} }
// WithRouter selects the HTTP router backend (default: the zero-dependency
// stdlib net/http router). Pass an opt-in adapter to switch:
//
// nexus.Boot(nexus.WithRouter(ginrouter.New()))
// nexus.Run(cfg, nexus.WithRouter(chirouter.New()))
//
// Equivalent to setting Config.Router. One line, no nexus.toml plumbing, and
// trivial to change — selecting gin/chi pulls their deps into the build, while
// the default links no third-party router at all.
func WithRouter(r httpx.Router) Option { return routerOption{r: r} }
// Module groups options under a name. Mirrors di.Module's logging — the
// group name appears in startup/shutdown logs and in error messages, which
// helps when several modules touch the same service or resource. The name
// is also stamped onto every AsQuery/AsMutation/AsRest registration inside
// the module so the dashboard's architecture view can group endpoints by
// module container.
//
// var advertsModule = nexus.Module("adverts",
// nexus.Provide(NewAdvertsService),
// nexus.AsQuery(NewGetAllAdverts),
// nexus.AsMutation(NewCreateAdvert, …),
// )
func Module(name string, opts ...Option) Option {
// Collect any RoutePrefix declarations among the direct children
// so we can stamp them on REST registrations. Multiple prefixes
// in the same Module concatenate left-to-right:
// Module("x", RoutePrefix("/a"), RoutePrefix("/b"), ...) → "/a/b".
//
// PublicPath is consumed alongside RoutePrefix — it's a sugar
// that means "this is the module's URL prefix" and must apply
// to REST mounts the same way RoutePrefix does. It ALSO seeds
// the module GraphQL path registry so app.Service(<modName>)
// returns a Service rooted at <path>/graphql.
var prefix string
var publicPath string
for _, o := range opts {
if rp, ok := o.(routePrefixOption); ok {
prefix += rp.prefix
}
if pp, ok := o.(pathOption); ok {
// Use the normalized form here so "/" is treated as a
// no-op prefix (existing module semantics) while
// AsComponent's Apply still sees the raw "/" as a
// literal root-URL mount.
normalized := pp.normalizedPath()
publicPath = normalized
prefix += normalized
}
}
// Register the module's GraphQL path BEFORE the children walk
// below. Module-aware children read the registry indirectly
// (via app.Service at construction time), so the registration
// only needs to land before di.Start fires constructors —
// which happens after this whole Module() call returns.
if publicPath != "" {
registerModulePublicPath(name, publicPath)
}
// Stamp module name + route prefix onto every child option that
// cares. Options produced by nested Module(...) don't implement
// these annotator interfaces (they return a rawOption wrapping
// di.Module), so inner-most wins automatically — the inner
// Module() already annotated its own children before we see it.
for _, o := range opts {
if ma, ok := o.(moduleAnnotator); ok {
ma.setModule(name)
}
if prefix != "" {
if rp, ok := o.(restPrefixAnnotator); ok {
rp.setRestPrefix(prefix)
}
}
}
return rawOption{o: di.Module(name, unwrap(opts)...)}
}
// Options bundles multiple Option values into a single Option.
// Useful when one logical feature expands into several: a
// conditional gate that pulls in a frontend mount + a config
// supply + an extra invoke, for example. Empty input is a no-op.
func Options(opts ...Option) Option {
if len(opts) == 0 {
return rawOption{o: di.Options()}
}
return rawOption{o: di.Options(unwrap(opts)...)}
}
// moduleAnnotator is implemented by options that participate in the
// nexus.Module grouping — specifically AsQuery/AsMutation/AsRest. The
// Module() function walks its direct children and calls setModule on
// each implementer so the registered endpoint knows its module.
type moduleAnnotator interface {
setModule(name string)
}
// Provide registers one or more constructor functions with the dep
// graph and auto-detects two opt-in extensions:
//
// - Resource providers: any returned value implementing
// NexusResourceProvider has its resource.Resource list registered
// with the app at boot. Add UseReporter alongside and OnResourceUse
// wires automatically — service→resource edges appear on first
// UsingCtx call without manual plumbing.
//
// - Service wrappers: when the first return is a *T whose struct
// anonymously embeds *nexus.Service, the constructor's params are
// scanned for resource providers and other service wrappers. The
// resulting (resourceDeps, serviceDeps) lists are recorded on the
// service's registry entry so the dashboard's architecture view
// draws service→service and service→resource edges at the SERVICE
// layer with no extra annotation.
//
// Constructors that don't trigger either detector behave like plain
// di.Provide — return types enter the graph, params resolve from it.
// Mixed sets (one service wrapper + one resource manager + one plain
// helper) work in a single call.
//
// nexus.Provide(
// NewDBManager, // resource provider — auto-registered
// NewCacheManager, // ditto
// NewAdvertsService, // service wrapper — deps recorded
// NewClock, // plain type — just enters the graph
// )
func Provide(fns ...any) Option {
opts := make([]di.Option, 0, len(fns)+1)
opts = append(opts, di.Provide(fns...))
for _, fn := range fns {
if inv := resourceAutoRegisterInvoke(fn); inv != nil {
opts = append(opts, inv)
}
if inv := serviceDepsRegisterInvoke(fn); inv != nil {
opts = append(opts, inv)
}
if inv := manifestAutoRegisterInvoke(fn); inv != nil {
opts = append(opts, inv)
}
}
return rawOption{o: di.Options(opts...)}
}
// Supply puts concrete values into the graph (no constructor). Useful for
// config structs or pre-built instances created outside the fx graph.
//
// nexus.Supply(nexus.Config{Server: ServerConfig{Addr: ":8080"}}) // rare — Run takes Config directly
// nexus.Supply(myAlreadyBuiltClient) // typical
func Supply(values ...any) Option {
return rawOption{o: di.Supply(values...)}
}
// Error injects an error discovered while building options; it surfaces at boot
// instead of panicking at call time. Extensions use it to report bad config
// without importing the DI backend.
//
// if err := cfg.validate(); err != nil { return nexus.Error(err) }
func Error(err error) Option { return rawOption{o: di.Error(err)} }
// Invoke runs a function at startup, resolving its parameters from the
// graph. Use for side-effects on boot — attaching resources, registering
// hooks, seeding state. Multiple Invoke options run in registration order.
//
// nexus.Invoke(func(app *nexus.App, dbs *DBManager) {
// app.OnResourceUse(dbs)
// })
func Invoke(fns ...any) Option {
return rawOption{o: di.Invoke(fns...)}
}
// serviceDepsRegisterInvoke synthesizes an di.Invoke that takes the
// constructed service + ALL of the constructor's original params,
// walks them for NexusResourceProvider / service-wrapper values, and
// calls registry.SetServiceDeps with the resulting name lists.
// Returns nil when fn isn't a function or its return isn't a
// service wrapper — letting ProvideService degrade to a plain
// Provide without failing boot.
func serviceDepsRegisterInvoke(fn any) di.Option {
rt := reflect.TypeOf(fn)
if rt == nil || rt.Kind() != reflect.Func || rt.NumOut() == 0 {
return nil
}
serviceType := rt.Out(0)
if !isServiceWrapperType(serviceType) {
return nil
}
// Invoke signature: (serviceType, param0, param1, ...) — fx will
// resolve each from the graph the same way it resolved them for
// the constructor itself.
in := make([]reflect.Type, 0, rt.NumIn()+1)
in = append(in, serviceType)
for i := 0; i < rt.NumIn(); i++ {
in = append(in, rt.In(i))
}
invokeType := reflect.FuncOf(in, nil, false)
invokeFn := reflect.MakeFunc(invokeType, func(args []reflect.Value) []reflect.Value {
svc, ok := unwrapService(args[0], serviceType)
if !ok || svc == nil {
return nil
}
owning := svc.Name()
var resourceDeps []string
var serviceDeps []string
// args[0] is the constructed service itself; args[1:] mirror
// the constructor's declared params in order.
for i := 1; i < len(args); i++ {
argType := rt.In(i - 1)
argVal := args[i]
if !argVal.IsValid() {
continue
}
if provider, ok := argVal.Interface().(NexusResourceProvider); ok {
for _, r := range provider.NexusResources() {
resourceDeps = append(resourceDeps, r.Name())
}
}
if isServiceWrapperType(argType) {
if depSvc, ok := unwrapService(argVal, argType); ok && depSvc != nil && depSvc.Name() != owning {
serviceDeps = append(serviceDeps, depSvc.Name())
}
}
}
svc.app.Registry().SetServiceDeps(owning, resourceDeps, serviceDeps)
return nil
})
return di.Invoke(invokeFn.Interface())
}
// resourceAutoRegisterInvoke synthesizes an di.Invoke(func(app *App, instance T))
// that, at boot, registers resources and wires OnResourceUse for the instance.
// Returns nil when fn isn't a function, returns nothing, or its first
// return type doesn't implement NexusResourceProvider or UseReporter —
// skipping the invoke avoids forcing a *App dep on the graph for plain
// types (a regression that surfaces when nexus.Provide is used for
// unrelated values like func() string in tests).
func resourceAutoRegisterInvoke(fn any) di.Option {
rt := reflect.TypeOf(fn)
if rt == nil || rt.Kind() != reflect.Func || rt.NumOut() == 0 {
return nil
}
// First return is the constructed instance. Ignore trailing error return.
outType := rt.Out(0)
providerIface := reflect.TypeOf((*NexusResourceProvider)(nil)).Elem()
reporterIface := reflect.TypeOf((*UseReporter)(nil)).Elem()
if !outType.Implements(providerIface) && !outType.Implements(reporterIface) {
return nil
}
invokeType := reflect.FuncOf(
[]reflect.Type{reflect.TypeOf((*App)(nil)), outType},
nil, false,
)
invokeFn := reflect.MakeFunc(invokeType, func(args []reflect.Value) []reflect.Value {
app := args[0].Interface().(*App)
inst := args[1].Interface()
if p, ok := inst.(NexusResourceProvider); ok {
for _, r := range p.NexusResources() {
app.Register(r)
}
}
if reporter, ok := inst.(UseReporter); ok {
app.OnResourceUse(reporter)
}
return nil
})
return di.Invoke(invokeFn.Interface())
}
// Raw is an escape hatch: accept any di.Option and route it through nexus.
// For low-level container wiring or one-off integrations. Normal apps never
// need it.
//
// nexus.Raw(di.Provide(myCtor))
func Raw(opt di.Option) Option {
return rawOption{o: opt}
}
// Run builds and runs the app. Blocks until SIGINT/SIGTERM, then
// gracefully shuts the HTTP server + cron scheduler. Returns nothing —
// identical to di.App.Run(). For tests where you need explicit Start/Stop
// control, build the app via a test helper that calls fxBootOptions.
//
// func main() {
// nexus.Run(
// nexus.Config{Addr: ":8080", EnableDashboard: true},
// nexus.Provide(NewDBManager),
// advertsModule,
// )
// }
//
// When NEXUS_FX_QUIET=1 is set in the environment, fx's startup log
// (PROVIDE/INVOKE/HOOK lines) is suppressed. The splitter sets this
// in subprocesses so the prefixed log streams don't drown in fx
// scaffolding noise; users hitting framework-level issues can unset
// it for full diagnostics.
// Boot loads nexus.toml automatically — the [runtime] Config, every
// [extensions.*] block, the [env] bridge, and the nexus.Get base
// layer — then runs the app. It's the zero-boilerplate form of:
//
// cfg := nexus.MustLoadConfig()
// opts := nexus.MustLoadExtensions()
// nexus.Run(cfg, append(opts, userOpts...)...)
//
// so main() collapses to:
//
// func main() {
// nexus.Boot(
// nexus.ServeFrontend(webFS, "web/dist"),
// billing.Module,
// )
// }
//
// A missing nexus.toml is tolerated (zero Config, no extensions) so
// apps without one still boot; a malformed one panics, matching the
// MustLoad* helpers. Override the path with the NEXUS_CONFIG env var,
// or call BootFrom(path, opts...).
//
// Run stays available for apps that build Config in Go or want
// explicit control over load order — Boot is sugar over it. Note that
// extension PACKAGES still need their blank import (Go links only
// imported code); Boot removes the load calls, not the imports.
func Boot(opts ...Option) {
BootFrom(resolveConfigPath(), opts...)
}
// BootFrom is Boot with an explicit nexus.toml path.
func BootFrom(path string, opts ...Option) {
cfg, extOpts := autoLoad(path)
Run(cfg, append(extOpts, opts...)...)
}
// resolveConfigPath picks the nexus.toml path in priority order:
//
// 1. NEXUS_CONFIG env override — always wins when set.
// 2. DefaultConfigPath ("nexus.toml") in the current working directory —
// the dev-time convention (cwd == project root).
// 3. nexus.toml sitting next to the executable — the deploy convention.
// A binary shipped with its config beside it (./oats_app +
// ./nexus.toml) then binds the configured port no matter which
// directory it's launched from, instead of silently falling back to
// the framework default (:8080) when cwd has no toml.
//
// The cwd copy is tried first so a `nexus dev` / `go run` from the project
// root keeps reading the source-tree toml even when a built binary also
// sits nearby.
func resolveConfigPath() string {
if p := os.Getenv("NEXUS_CONFIG"); p != "" {
return p
}
if _, err := os.Stat(DefaultConfigPath); err == nil {
return DefaultConfigPath
}
if exe, err := os.Executable(); err == nil {
beside := filepath.Join(filepath.Dir(exe), DefaultConfigPath)
if _, err := os.Stat(beside); err == nil {
return beside
}
}
// Nothing found anywhere — return the conventional path so autoLoad's
// ErrNotExist branch runs (and warns) with a familiar name.
return DefaultConfigPath
}
// autoLoad reads runtime Config + extension options for Boot. It
// resolves the TOML source in priority order:
//
// 1. the disk file at path (NEXUS_CONFIG → cwd → next to the executable,
// via resolveConfigPath) — an operator's on-disk config always wins,
// so a deployed binary can be re-tuned without a rebuild;
// 2. the copy embedded at build time by `nexus build` (config_embed.go),
// so a single self-contained binary carries its own defaults;
// 3. nothing — framework defaults, with a loud warning (a silently
// dropped config was the classic "why is it on :8080?" footgun).
//
// A malformed config (disk or embedded) panics so misconfiguration fails
// loudly at startup rather than silently dropping settings.
func autoLoad(path string) (Config, []Option) {
raw, err := readFileIfExists(path)
if err != nil {
// a real I/O error (perms, etc.) — not a soft miss
panic(fmt.Errorf("nexus: failed to read config %q: %w", path, err))
}
source := path
if raw == nil {
if emb, ok := embeddedConfig(); ok {
raw, source = emb, "embedded nexus.toml"
}
}
if raw == nil {
// No config anywhere. Tolerated so config-less apps still boot —
// but it silently drops every setting a file would carry (listen
// addr included, so the app falls back to :8080). That has bitten
// deployments launched from a directory without their toml, so
// make it loud on stderr rather than a mystery default port.
fmt.Fprintf(os.Stderr,
"nexus: no %s found (looked in cwd, next to the executable, and the "+
"build-time embed); using framework defaults — listen addr falls "+
"back to :8080. Set NEXUS_CONFIG, run from the config's directory, "+
"or `nexus build` to embed it.\n",
DefaultConfigPath)
return Config{}, nil
}
cfg, err := configFromTOML(raw, source)
if err != nil {
panic(fmt.Errorf("nexus: malformed config (%s): %w", source, err))
}
extOpts, err := decodeExtensions(raw)
if err != nil {
panic(fmt.Errorf("nexus: malformed [extensions.*] in config (%s): %w", source, err))
}
// Dev boot self-check: run the same config lint `nexus lint` runs, but at
// boot in dev, so a bad CIDR / CORS combo / rate limit / unimported
// extension surfaces now instead of only when someone remembers to lint.
// Advisory (reported by runBootChecks, never aborts); prod pays nothing.
if IsDev() {
if issues, lerr := lintRuntimeBytes(raw, source); lerr == nil {
addPendingBootIssues(issues)
}
}
return cfg, extOpts
}
// Run starts an app from a Config you build in Go, plus the given options
// (modules, Provide, AsRest/AsQuery/AsWS, extension modules). It blocks until
// the process is signalled to stop.
//
// Most apps should call Boot instead — it loads Config + [extensions.*] from
// nexus.toml and is sugar over Run. Reach for Run when Config carries values
// TOML can't express (a shared Store, a middleware func slice, a pluggable
// router/container backend) or when you want explicit control over load order.
// For tests, use InProcess (no listener). See the package doc for the full
// entry-point rundown.
func Run(cfg Config, opts ...Option) {
// Print-mode short-circuit. When NEXUS_PRINT_MANIFEST=1 is set,
// the orchestration platform is invoking us at build/upload time
// to extract the manifest. Build the fx graph, populate *App
// (which fires every DeclareEnv / DeclareService / UseVolume /
// AddStartupTask invoke from module-level options), print the
// manifest as JSON, exit 0. Lifecycle hooks never run — no
// listener bind, no DB/Redis dial.
//
// Side-effect contract: implementations of EnvProvider /
// ServiceDependencyProvider / VolumeProvider, and any constructor
// that fx invokes during graph build, must be cheap and free of
// network/filesystem reads. fx is lazy by default, so this holds
// for typical apps.
if os.Getenv(printManifestEnv) == "1" {
printManifestAndExitIfRequested(cfg, opts)
return // unreachable; printManifestAndExitIfRequested calls os.Exit
}
// Quiet-by-default in dev: nexus dev sets NEXUS_DEV=1 on the
// child, which here implies "suppress [Fx] graph chatter and
// [GIN-debug] route-registration spam unless the user wants
// them back". The opt-out is NEXUS_VERBOSE=1 (set by the
// `nexus dev --verbose` flag). Users running `go run` directly
// keep today's behavior — neither env var is set.
devQuiet := os.Getenv("NEXUS_DEV") == "1" && os.Getenv("NEXUS_VERBOSE") != "1"
if devQuiet {
// Don't override an explicit GIN_MODE — operators sometimes
// pin it for reasons we can't see (CI, container images).
if os.Getenv("GIN_MODE") == "" {
_ = os.Setenv("GIN_MODE", "release")
}
}
// Two-phase split: fxEarlyOptions seeds Config + *App + lifecycle
// BEFORE user opts run, then user opts (which may install global
// middleware via auth.Module / engine.Use), then fxLateOptions
// runs autoMountGraphQL last so GraphQL routes pick up every
// user-installed middleware. Without the split, GraphQL routes
// registered first wouldn't see middleware Use()'d afterwards
// — gin captures middleware at route-registration time.
//
// autoManifestOptions sits between Early and the user opts so
// any plugin the user declares can read its per-environment
// block from app.EffectiveManifest() at boot without the
// operator having to write a LoadDeployManifest invoke.
// Resolve the router backend before the graph is built: New(cfg)
// (inside fxEarlyOptions) constructs the default router, so a
// WithRouter option must seed Config.Router up front.
backend := di.Backend(di.Builtin())
for _, o := range opts {
if ro, ok := o.(routerOption); ok {
cfg.Router = ro.r
}
if co, ok := o.(containerOption); ok && co.backend != nil {
backend = co.backend
}
}
all := append([]di.Option{
fxEarlyOptions(cfg),
autoManifestOptions(),
}, unwrap(opts)...)
// Deferred sources (e.g. nexus/decorate's //@-annotation drain) contribute
// AFTER the app's own options and BEFORE autoMountGraphQL, so their
// endpoints take part in schema assembly like any hand-written module.
all = append(all, unwrap(collectDeferredOptions())...)
all = append(all, fxLateOptions())
// Bound the whole stop chain, not just the HTTP drain. The listener
// hook already caps its own Shutdown; this covers everything after
// it (db/cache Close, worker cancel) so no single wedged resource can
// hold the process open. Sized above the HTTP window so a normal
// drain never trips it.
all = append(all, Raw(di.WithStopTimeout(shutdownTimeout(cfg)+5*time.Second)).nexusOption())
// Dev boot self-check runs LAST as an invoke — after pubsub's BindTopics
// and every other wiring invoke — so live-topology checks (e.g. "topic has
// no transport bound") see the finalized graph. Dev-only: no invoke, no
// cost in production.
if IsDev() {
all = append(all, Invoke(func() { runBootChecks() }).nexusOption())
// Snapshot preserved in-memory state on the way out, so the binary
// `nexus dev` is about to swap in can pick it up (see devstate.go).
// Registered last => runs first on shutdown, before resources close.
all = append(all, Invoke(func(lc Lifecycle) {
lc.Append(Hook{OnStop: func(context.Context) error {
return devStates.writeDevState()
}})
}).nexusOption())
}
// The builtin container prints build/start errors to stderr itself; the
// opt-in fx adapter owns its own logging (and honors NEXUS_FX_QUIET).
// devQuiet only governs the gin route-registration spam, set above.
_ = devQuiet
backend.Build(di.Collect(all...)).Run()
}
// unwrap flattens a []Option into the []di.Option the container needs.
func unwrap(opts []Option) []di.Option {
out := make([]di.Option, len(opts))
for i, o := range opts {
out[i] = o.nexusOption()
}
return out
}