@flamework-experimental/core 2.0.0-alpha.2 → 2.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +41 -34
  2. package/docs/README.md +63 -0
  3. package/docs/guide/01-getting-started.md +334 -0
  4. package/docs/guide/02-modules.md +254 -0
  5. package/docs/guide/03-providers.md +423 -0
  6. package/docs/guide/04-lifecycle-events.md +423 -0
  7. package/docs/guide/05-components.md +793 -0
  8. package/docs/guide/06-networking.md +614 -0
  9. package/docs/guide/07-macros.md +332 -0
  10. package/docs/guide/08-plugins.md +203 -0
  11. package/docs/guide/09-project-structure.md +392 -0
  12. package/docs/guide/10-migrating-from-v1.md +573 -0
  13. package/docs/guide/11-scopes.md +165 -0
  14. package/docs/guide/12-testing.md +342 -0
  15. package/flamework.build +1 -1
  16. package/out/dependency.d.ts +4 -0
  17. package/out/dependency.luau +4 -0
  18. package/out/index.d.ts +5 -2
  19. package/out/init.luau +11 -2
  20. package/out/lifecycle/lifecyclePlugin.d.ts +13 -1
  21. package/out/lifecycle/lifecyclePlugin.luau +67 -7
  22. package/out/module/module.luau +126 -24
  23. package/out/module/moduleBuilder.d.ts +12 -3
  24. package/out/module/moduleBuilder.luau +23 -1
  25. package/out/module/moduleDefinition.d.ts +6 -0
  26. package/out/module/providerRegistration.d.ts +8 -0
  27. package/out/module/providerRegistration.luau +29 -0
  28. package/out/plugin/pluginDefinition.d.ts +7 -4
  29. package/out/provider.d.ts +19 -0
  30. package/out/provider.luau +8 -0
  31. package/out/reflect.luau +6 -0
  32. package/out/utility/explainUnresolved.d.ts +9 -0
  33. package/out/utility/explainUnresolved.luau +43 -0
  34. package/out/utility/getClassesInPath.d.ts +37 -3
  35. package/out/utility/getClassesInPath.luau +154 -33
  36. package/out/utility/globs.d.ts +2 -2
  37. package/out/utility/globs.luau +3 -3
  38. package/out/utility/leftOut.d.ts +35 -0
  39. package/out/utility/leftOut.luau +171 -0
  40. package/out/utility/moduleClasses.d.ts +9 -0
  41. package/out/utility/moduleClasses.luau +64 -0
  42. package/out/utility/pathRoot.d.ts +20 -1
  43. package/out/utility/pathRoot.luau +80 -4
  44. package/package.json +14 -7
@@ -0,0 +1,254 @@
1
+ # 2. Modules
2
+
3
+ A **module** is the object `Flamework.createModule()` builds. It is two things at once:
4
+
5
+ 1. **A dependency-injection container.** It holds a set of providers, and gives each provider the
6
+ other providers it depends on.
7
+ 2. **A lifecycle unit.** It starts (*ignites*) as a whole and stops (*extinguishes*) as a whole.
8
+
9
+ In v1 there was exactly one module, and it was global and implicit. In v2 you create it yourself,
10
+ which is what makes tests and tools possible. But **most games have exactly one module per realm and
11
+ never extinguish it**. If that is your game, the second half of this page is optional reading.
12
+
13
+ ## The one-module case
14
+
15
+ ```ts
16
+ Flamework.createModule()
17
+ .registerProviders("src/server/services")
18
+ .ignite();
19
+ ```
20
+
21
+ That is all a typical game needs. You never call `build()` or `extinguish`, and `Dependency<T>()`
22
+ reaches this module from anywhere.
23
+
24
+ ## The builder
25
+
26
+ `Flamework.createModule()` returns a `ModuleBuilder`. Every method except `build()` and `ignite()`
27
+ returns the builder, so those calls chain. `build()` returns a `ModuleDefinition`, and `ignite()`
28
+ returns the ignited `Module`.
29
+
30
+ | Method | Does |
31
+ |---|---|
32
+ | `registerProviders(path)` | Registers every `@Provider()` class defined in the files under a source folder, exported or not. |
33
+ | `registerProvidersGlob(glob)` | The same, for every folder a glob matches, resolved when you build. |
34
+ | `registerClassProvider(Class)` | Registers one class explicitly. |
35
+ | `registerProvider<T>(config, id?)` | Registers a class, function or alias provider. |
36
+ | `includePlugin(plugin)` | Adds a plugin, which can hook into this module. |
37
+ | `disableDefaultLifecycle()` | Leaves out the `LifecyclePlugin` every module starts with. |
38
+ | `setDebugName(name)` | Names the module in error messages. |
39
+ | `apply(fn)` | Runs `fn(builder)` without breaking the chain. |
40
+ | `build()` | Finishes the builder and returns a `ModuleDefinition`. |
41
+ | `ignite(options?)` | Shorthand for `.build().ignite()`. `{ default: true }` makes this the module `Dependency<T>()` answers from. |
42
+
43
+ ### `build()` vs `ignite()`
44
+
45
+ `ignite()` is `build().ignite()`. Use `build()` when the module is going to be ignited later, or more
46
+ than once:
47
+
48
+ ```ts
49
+ // Full form
50
+ const definition = Flamework.createModule().registerClassProvider(Economy).build();
51
+ const module = definition.ignite();
52
+
53
+ // Shorthand, when you do not need the definition
54
+ const module = Flamework.createModule().registerClassProvider(Economy).ignite();
55
+ ```
56
+
57
+ You can ignite a definition many times, and **each ignition gets its own provider instances**. That
58
+ makes a module a clean unit for tests: build the definition once, and ignite a fresh module for each
59
+ test.
60
+
61
+ ## What ignition does
62
+
63
+ In order:
64
+
65
+ 1. Plugins are set up. Each plugin's setup function runs against this module and registers
66
+ providers, hooks and observers into it. A plugin reached twice is set up once.
67
+ 2. `onPreIgnite` hooks run.
68
+ 3. Every registered provider is constructed, and its constructor dependencies are resolved.
69
+ 4. `onPostIgnite` hooks run. This is where `LifecyclePlugin` calls `onInit` on everything that
70
+ implements it. An error raised up to this point fails the ignition.
71
+ 5. The module is now ignited, and `onIgnited` hooks run. This is where `LifecyclePlugin` calls
72
+ `onStart` on everything that implements it, and then starts its `RunService` connections. So an
73
+ `onStart` sees `isIgnited()` return `true`, may `extinguish()` the module, and may ignite a module
74
+ that imports it.
75
+
76
+ Within step 3, providers are constructed on demand: resolving a dependency constructs it if it does
77
+ not exist yet. So a provider's constructor can safely use anything injected into it.
78
+
79
+ ## Resolving by hand
80
+
81
+ ```ts
82
+ const shop = module.resolveDependency<Shop>();
83
+ ```
84
+
85
+ Use this where Flamework meets code that it does not manage. Inside a provider, take a constructor
86
+ parameter instead.
87
+
88
+ Some code has no module handle at hand: a UI component, a script, a callback registered with
89
+ something outside Flamework. There, `Dependency<T>()` resolves against the **default module**:
90
+
91
+ ```ts
92
+ import { Dependency } from "@flamework-experimental/core";
93
+
94
+ const shop = Dependency<Shop>();
95
+ ```
96
+
97
+ The first module ignited in a realm is the default. In a game, that is the one the entry point
98
+ ignites. To make a later module the default instead, pass `{ default: true }`:
99
+
100
+ ```ts
101
+ const module = definition.ignite({ default: true });
102
+ ```
103
+
104
+ Extinguishing the default module releases it, and the next module ignited becomes the default. So a
105
+ test that ignites and extinguishes a module per case never leaks one into the next. A module that
106
+ fails to ignite never becomes the default: if one ignited with `{ default: true }` raises, the
107
+ previous default stays as it was. With no default, `Dependency<T>()` raises
108
+ `Dependency<T>() was called before any module was ignited`.
109
+
110
+ With more than one module running, pass the one to resolve from. This is the same as
111
+ `module.resolveDependency<T>()`, for code that has the handle but prefers the shape of the global
112
+ function:
113
+
114
+ ```ts
115
+ const shop = Dependency<Shop>(worldModule);
116
+ ```
117
+
118
+ A provider can also inject the module itself:
119
+
120
+ ```ts
121
+ @Provider()
122
+ class Registry {
123
+ constructor(private module: Module) {}
124
+
125
+ public spawnSession() {
126
+ return this.module.createClassInstance(Session);
127
+ }
128
+ }
129
+ ```
130
+
131
+ ## Tearing down
132
+
133
+ ```ts
134
+ module.extinguish();
135
+ ```
136
+
137
+ This runs the `onExtinguished` hooks, releases the instances the module created, and unregisters
138
+ them from every plugin observing them. So the lifecycle plugin stops ticking providers that are gone.
139
+
140
+ Games rarely call this. Tests do, and so do tools that mount and unmount.
141
+
142
+ ## More than one module?
143
+
144
+ A second module is a second container. Nothing in one module can inject anything from the other
145
+ unless it imports it, and `Dependency<T>()` answers from only one of them. Use a second module only
146
+ when you want that separation:
147
+
148
+ - **Tests**, where each case wants a fresh container. Build the definition once, and ignite it for
149
+ each case.
150
+ - **A tool** that lives for less time than the game, and is extinguished when it closes.
151
+ - **A scenario** that runs against the game, such as a test rig or a debug world, and is torn down on
152
+ its own. It imports the game module (see below).
153
+
154
+ ### Importing a module
155
+
156
+ A module ignited with `imports` can inject and resolve the providers of the modules it lists. It
157
+ looks in its own providers first:
158
+
159
+ ```ts
160
+ const game = Flamework.createModule()
161
+ .registerProviders("src/server/services")
162
+ .ignite();
163
+
164
+ const rig = Flamework.createModule()
165
+ .registerProviders("src/server/Testing/rig")
166
+ .ignite({ imports: [game] });
167
+ ```
168
+
169
+ A provider in `rig` can take `DataService` in its constructor, just as a provider in `game` can.
170
+ Resolution looks in `rig` first, then in each import in order, and each import also searches its own
171
+ imports. When nothing is found, the error names the imports it searched. Nothing is copied: the
172
+ import keeps its providers, their lifecycle, their observers and their extinguish, and `rig` only
173
+ resolves them. A lazy provider of the import is constructed by the import, the first time either
174
+ module asks for it.
175
+
176
+ Every import has to be ignited first. `ignite()` is synchronous, so in one entry script this is just
177
+ the order of the lines. If you get it wrong, `ignite()` raises `imported module '...' is not ignited`
178
+ before anything in the importer is constructed.
179
+
180
+ Two rules decide what happens when both modules register the same id (the string Flamework uses to
181
+ identify a class):
182
+
183
+ - **The same class is shared.** If the importer registers a class that an import already resolves
184
+ to, the importer's registration is dropped and the import's instance answers. So a folder that
185
+ both modules' paths match does not produce two of everything.
186
+ `registerClassProvider(Class, { isolated: true })` keeps a separate instance in the importer
187
+ instead.
188
+ - **A different class wins.** `rig.registerProvider<DataService>({ type: "class", value: FakeDataService })`
189
+ is kept, and answers before the import's `DataService`. This is how a scenario replaces one of the
190
+ game's providers with a fake, for itself only. The game keeps the real one.
191
+
192
+ Extinguishing an import first extinguishes every module that imports it, deepest first. So
193
+ `game.extinguish()` takes `rig` down before the game. An importer extinguished on its own detaches,
194
+ and the import keeps running.
195
+
196
+ Some things that used to need a second module are now a [plugin](08-plugins.md): a library that
197
+ ships providers, or code both realms share. A plugin's setup registers the providers into whichever
198
+ module includes it. A plugin that two other plugins both include is set up once.
199
+
200
+ ```ts
201
+ // src/shared/plugins/core.ts
202
+ export const CorePlugin = Flamework.createPlugin("Core", (target) => {
203
+ target.registerProviders("src/shared/services");
204
+ });
205
+
206
+ // both entry points
207
+ .includePlugin(CorePlugin)
208
+ ```
209
+
210
+ Each realm ignites its own module, which is what you want: the server and the client are different
211
+ processes.
212
+
213
+ ## Patterns
214
+
215
+ **A module per test.** Build the definition once, ignite it for each case, and extinguish it
216
+ afterwards:
217
+
218
+ ```ts
219
+ const definition = Flamework.createModule().registerClassProvider(Shop).build();
220
+
221
+ const module = definition.ignite();
222
+ // ...assert...
223
+ module.extinguish();
224
+ ```
225
+
226
+ **`apply` for conditional wiring**, so the chain stays readable:
227
+
228
+ ```ts
229
+ Flamework.createModule()
230
+ .apply((builder) => (RunService.IsStudio() ? builder.registerClassProvider(DebugTools) : builder))
231
+ .ignite();
232
+ ```
233
+
234
+ ## Caveats
235
+
236
+ - **A module ignites once and extinguishes once.** Igniting a `Module` twice, or extinguishing it
237
+ twice, raises `module is in invalid state when transitioning to '...'`. If you want a second
238
+ container, ignite the *definition* again.
239
+ - **You cannot resolve during plugin setup or `onPreIgnite`.** Providers do not exist yet. The error
240
+ `module is in pre-ignite phase, dependency cannot be resolved` means a plugin tried. Register state
241
+ early, and resolve in `onPostIgnite`.
242
+ - **`Dependency<T>()` answers from one module.** It uses the first module ignited, unless a later one
243
+ was ignited with `{ default: true }`. A realm with two running modules (tests, tools) should say
244
+ which one, or resolve through the module handle.
245
+ - **Duplicate registration raises at ignition.** `provider ID was registered more than once` usually
246
+ means two `registerProviders` paths overlap. It can also mean a class is registered both by path
247
+ and by hand, or by both the module and a plugin. Two registrations are fine when their
248
+ [scope conditions](11-scopes.md) keep at most one of them.
249
+ - **Imports are one way.** A module sees its imports' providers, but an import never sees the
250
+ importer's. A fake registered in the importer replaces nothing in the import.
251
+
252
+ ---
253
+
254
+ Previous: [Getting started](01-getting-started.md) · Next: [Providers](03-providers.md)
@@ -0,0 +1,423 @@
1
+ # 3. Providers
2
+
3
+ A **provider** is usually a class that its module creates once: a singleton within that module.
4
+ (Other kinds are covered under [Other kinds of provider](#other-kinds-of-provider).) You write most
5
+ of your game as providers.
6
+
7
+ ```ts
8
+ import { Provider } from "@flamework-experimental/core";
9
+
10
+ @Provider()
11
+ export class Economy {
12
+ public balance = 0;
13
+ }
14
+ ```
15
+
16
+ `@Provider()` does two things. It marks the class as a provider, and it tells the transformer to
17
+ attach the metadata that dependency injection needs: the class's identifier (id), its constructor
18
+ parameter types, and the interfaces it implements.
19
+
20
+ ## Registration
21
+
22
+ **You do not list your providers by hand.** `registerProviders` takes a folder:
23
+
24
+ ```ts
25
+ Flamework.createModule().registerProviders("src/server/services").ignite();
26
+ ```
27
+
28
+ This is v2's version of v1's `Flamework.addPaths(...)`. Use it for ordinary game code.
29
+
30
+ ### How it actually works
31
+
32
+ It helps to know this, because the caveats follow from it:
33
+
34
+ 1. **At compile time**, the transformer uses your Rojo project file to turn `"src/server/services"`
35
+ into the Rojo path that the folder ends up at. This is why the argument must be a string literal,
36
+ and why the folder must be mapped.
37
+ 2. **At runtime**, Flamework finds that instance with `WaitForChild` and requires every
38
+ `ModuleScript` under it. It collects every Flamework class those ModuleScripts define, **exported
39
+ or not**. It also collects anything they export that carries Flamework metadata, such as a
40
+ re-export of a class from elsewhere. A registration whose own scope condition does not hold
41
+ skips this step: the folder is not looked up and nothing is required. See [Scopes](11-scopes.md).
42
+ 3. It keeps the classes marked as providers, and registers each one once under its generated id,
43
+ however many ways it was found.
44
+
45
+ So registration means "require everything in this folder and see what comes out", as it did in v1.
46
+
47
+ Unexported classes are found because the transformer records each class against the ModuleScript
48
+ that defines it (`script`). The id plays no part in this, so it works in every `idGenerationMode`
49
+ and with obfuscation on.
50
+
51
+ Only a class that the ModuleScript creates once, as it loads, is recorded: one declared at the top
52
+ level of the file, or at the top level of a namespace in it. A class declared inside a function is
53
+ created again by every call, so it is never recorded. That way, a later path registration never
54
+ picks up a class that belongs to a test case or a factory. Such a class is found only if its file
55
+ exports it.
56
+
57
+ Only classes that carry `@Provider()` **themselves** are registered. Metadata is inherited through
58
+ the class hierarchy, but an exported, undecorated subclass of a provider is still skipped, rather
59
+ than registered under its parent's id. Registering one explicitly raises an error.
60
+
61
+ ### Registering by glob
62
+
63
+ When your providers are spread over folders whose paths share a pattern, a glob saves listing each
64
+ folder:
65
+
66
+ ```ts
67
+ Flamework.createModule().registerProvidersGlob("src/server/**/services").ignite();
68
+ ```
69
+
70
+ The glob is resolved at **compile time** against your source tree, and the matching Rojo paths are
71
+ written to `include/flamework/globs.json`, which the runtime reads. Two consequences: the include
72
+ directory must be part of your Rojo project (it is in a default roblox-ts project), and only game
73
+ projects emit the file -- a published package cannot use globs. This is v1's `Flamework.addPathsGlob`.
74
+
75
+ A glob that matches no files is not an error, since a folder can be empty on purpose. It registers
76
+ nothing, and the build prints a warning with the glob and the file and line that use it.
77
+
78
+ ### Explicit registration
79
+
80
+ To register one specific class, such as a library's provider, a test double or something
81
+ conditional:
82
+
83
+ ```ts
84
+ // Shorthand: uses the class's generated identifier
85
+ .registerClassProvider(Economy)
86
+
87
+ // Full form: the same thing spelled out
88
+ .registerProvider<Economy>({ type: "class", value: Economy })
89
+ ```
90
+
91
+ Both raise `class 'X' is missing the @Provider() decorator` if the class is not decorated itself,
92
+ even when it inherits the decorator from a parent class.
93
+
94
+ ## Dependency injection
95
+
96
+ Constructor parameters are resolved by type:
97
+
98
+ ```ts
99
+ @Provider()
100
+ export class Shop {
101
+ constructor(
102
+ private economy: Economy,
103
+ private logger: Logger,
104
+ ) {}
105
+ }
106
+ ```
107
+
108
+ There is nothing to annotate. The transformer records each parameter's id, and the module resolves
109
+ them, constructing anything that does not exist yet.
110
+
111
+ Resolution looks in the module first: its own providers, and whatever its plugins registered or
112
+ provided. Then it looks in the modules this one imports, in order (see
113
+ [Importing a module](02-modules.md#importing-a-module)). Nothing else is searched.
114
+
115
+ You can also inject `Module` (the module doing the resolving) and anything a plugin provided. See
116
+ [Plugins](08-plugins.md).
117
+
118
+ ### Outside a provider
119
+
120
+ Code with no constructor, such as a UI component, a script or a signal handler, reaches a provider
121
+ through `Dependency<T>()`. It resolves against the default module: the first one ignited, or the one
122
+ ignited with `{ default: true }` (see [Modules](02-modules.md#resolving-by-hand)).
123
+
124
+ ```ts
125
+ import { Dependency } from "@flamework-experimental/core";
126
+
127
+ const economy = Dependency<Economy>();
128
+ ```
129
+
130
+ Prefer a constructor parameter wherever you can use one. It declares the dependency where readers can
131
+ see it, and it makes the module construct the dependency first. `Dependency<T>()` inside a
132
+ provider's constructor works, as it did in v1, but the module cannot see that dependency.
133
+
134
+ ### Circular dependencies
135
+
136
+ Two providers that inject each other cannot both be constructed first, and Flamework will not
137
+ untangle this for you. Break the cycle: inject `Module` into one of them, and resolve the other
138
+ lazily, only when it is used:
139
+
140
+ ```ts
141
+ @Provider()
142
+ class A {
143
+ constructor(private module: Module) {}
144
+
145
+ private get b() {
146
+ return this.module.resolveDependency<B>();
147
+ }
148
+ }
149
+ ```
150
+
151
+ Better still, look for the third provider: a cycle usually means one is trying to exist.
152
+
153
+ ### Asking for something that is not a provider
154
+
155
+ `Dependency<T>()`, `resolveDependency<T>()` and constructor injection resolve only **providers**. v1
156
+ built any decorated class on demand, but v2 does not. Asking for a class that is not a provider
157
+ fails:
158
+
159
+ - **A component** (`@Component()`) is built by `Components` on the instances it is attached to, never
160
+ by a module. When you build, the transformer refuses `Dependency<T>()`,
161
+ `module.resolveDependency<T>()` and a `@Provider()`'s constructor parameter when the type is a
162
+ component: `'QuestsUI' is a component (@Component), not a provider`. Make it a `@Provider()`: a
163
+ provider cannot extend `BaseComponent`, so move what callers need into a provider. Or get the
164
+ component from the instance: `components.getComponent<QuestsUI>(instance)`.
165
+ - **A `@Provider()` that nothing registers** raises at runtime. The error says so, and says where the
166
+ class is defined: `'Shop' (ServerScriptService.TS.shop) is a @Provider() that nothing in this
167
+ module registers or provides`. Register its folder, register the class, include the plugin that
168
+ provides it, or import a module that has it.
169
+ - **An `@Injectable()` class** is built with `createClassInstance`, never resolved:
170
+ `'Session' (...) is not a provider`.
171
+
172
+ The check made when you build covers only what the type makes certain. It never refuses:
173
+
174
+ - an id passed by hand (`Dependency<T>(undefined, id)`)
175
+ - an interface or abstract class, since a function or alias provider may stand behind it
176
+ - `Dependency<Components>()`, and anything else a plugin provides
177
+ - a `@Provider()` class
178
+ - a macro of your own that takes a `Modding.Target.Dependency<T>`
179
+
180
+ A component's own constructor may take another component: that is a component dependency. An
181
+ `@Injectable()`'s constructor may take one too, and `overrideDependency` can answer it. At runtime,
182
+ the full explanation is given only for a class that has loaded and was defined at the top level of
183
+ its file. Anything else gets the plain `could not resolve dependency 'X'`.
184
+
185
+ ## Other kinds of provider
186
+
187
+ A provider does not have to be a class.
188
+
189
+ ### Function providers
190
+
191
+ The callback is called on **every** resolution: once per constructor parameter that asks for it, and
192
+ once per `resolveDependency`. Nothing is cached for you, so if you want a singleton, cache it in the
193
+ callback:
194
+
195
+ ```ts
196
+ interface Config {
197
+ readonly maxPlayers: number;
198
+ }
199
+
200
+ .registerProvider<Config>({
201
+ type: "function",
202
+ callback: () => ({ maxPlayers: 8 }),
203
+ })
204
+ ```
205
+
206
+ The callback receives an `InjectionContext` describing *who asked*:
207
+
208
+ | Field | Is |
209
+ |---|---|
210
+ | `injectionId` | The id being resolved. |
211
+ | `dependencyInfo` | The id plus any metadata carried on the type. |
212
+ | `module` | The module resolving the dependency. |
213
+ | `origin` | The class being constructed, if any. |
214
+
215
+ `origin` lets you give each consumer its own logger:
216
+
217
+ ```ts
218
+ .registerProvider<Logger>({
219
+ type: "function",
220
+ callback: (context) => new Logger(tostring(context.origin)),
221
+ })
222
+ ```
223
+
224
+ Every class that injects a `Logger` gets one tagged with its own name. That is why the callback runs
225
+ on every resolution. For a shared value, create it outside the callback and close over it:
226
+
227
+ ```ts
228
+ const config = { maxPlayers: 8 };
229
+ .registerProvider<Config>({ type: "function", callback: () => config })
230
+ ```
231
+
232
+ ### Alias providers
233
+
234
+ An alias provider resolves one id to another. This is how an interface gets an implementation:
235
+
236
+ ```ts
237
+ .registerClassProvider(DataStoreStorage)
238
+ .registerProvider<Storage>({ type: "alias", injectionId: Flamework.id<DataStoreStorage>() })
239
+ ```
240
+
241
+ Anything that injects `Storage` now gets the `DataStoreStorage` instance: the same instance, not a
242
+ second one. In tests, swap the alias to swap the implementation.
243
+
244
+ ### Lazy providers
245
+
246
+ A provider is normally constructed during ignition, whether or not anything uses it. Mark it lazy to
247
+ construct it only when something first resolves it:
248
+
249
+ ```ts
250
+ @Provider({ lazy: true })
251
+ export class Telemetry implements OnStart {}
252
+ ```
253
+
254
+ A lazy provider that nothing ever resolves is never created. One that is resolved after ignition
255
+ still gets `onInit` and `onStart`, on the next resume point after it is constructed. From then on it
256
+ behaves like any other provider. One resolved during ignition, from another provider's `onInit`, is
257
+ initialised in order, before anything starts. This is v1's `@Optional()`. There is no equivalent
258
+ of `includeOptionalClass`, because resolving a lazy provider is how you include it.
259
+
260
+ ### Load order
261
+
262
+ `loadOrder` sets when a provider's `onInit` and `onStart` run, compared with the other providers of
263
+ the same ignition, as v1's `@Service({ loadOrder })` did. Lower values go first, and the default is
264
+ `1`. Providers with the same value keep the order they would have without one.
265
+
266
+ ```ts
267
+ @Provider({ loadOrder: 0 })
268
+ export class CameraShake implements OnStart {
269
+ public onStart() {} // started before the providers left at 1
270
+ }
271
+
272
+ @Provider({ loadOrder: 5 })
273
+ export class Interface implements OnStart {
274
+ public onStart() {} // started after them
275
+ }
276
+ ```
277
+
278
+ Dependencies still come first. A provider is constructed and initialised after what its constructor
279
+ takes, even when that has a higher `loadOrder`. So a low `loadOrder` pulls the provider's
280
+ dependencies forward with it. See [Lifecycle events](04-lifecycle-events.md#load-order) for the
281
+ exact order.
282
+
283
+ `loadOrder` has no effect on a lazy provider, which starts when it is first resolved. It also has no
284
+ effect across modules: an imported module ignites, and starts, before the module that imports it.
285
+ Any finite number is accepted. Anything else raises an error when the ModuleScript that defines the
286
+ class loads.
287
+
288
+ ### Scoped providers
289
+
290
+ A provider can be tied to the build's *scopes*, the names a build is compiled with. Then a test
291
+ scenario or a debug tool exists only in the builds that ask for it:
292
+
293
+ ```ts
294
+ @Provider({ activeIn: ["components"] })
295
+ export class ComponentProbe {}
296
+ ```
297
+
298
+ The same `activeIn`/`inactiveIn` pair also goes on a registration
299
+ (`registerProviders(path, { ... })`, `registerClassProvider(Class, { ... })`, or the config of
300
+ `registerProvider`) and on `ignite`. The conditions combine by AND: all of them must hold. A
301
+ provider that is left out is not registered at all. A path or glob registration whose own
302
+ condition does not hold does not even load its folder. See [Scopes](11-scopes.md).
303
+
304
+ ## Classes that are not providers
305
+
306
+ Sometimes you want dependency injection for a class you create yourself, such as a session, a
307
+ request or a per-player object. You do not want it to be a singleton, or to be picked up by
308
+ `registerProviders`. Use `@Injectable()`:
309
+
310
+ ```ts
311
+ import { Injectable } from "@flamework-experimental/core";
312
+
313
+ @Injectable()
314
+ class Session {
315
+ constructor(private economy: Economy) {}
316
+ }
317
+ ```
318
+
319
+ ```ts
320
+ const session = module.createClassInstance(Session);
321
+ ```
322
+
323
+ `@Injectable()` attaches the same metadata as `@Provider()`, but does **not** mark the class as a
324
+ provider. So path registration skips it, and it cannot be resolved by id.
325
+
326
+ The module owns the instance. The instance is attached to any lifecycle events it implements, and
327
+ released when the module extinguishes or when you release it yourself:
328
+
329
+ ```ts
330
+ module.removeClassInstance(session);
331
+ ```
332
+
333
+ Removing twice is safe and does nothing the second time.
334
+
335
+ ### Passing arguments
336
+
337
+ `createClassInstance` resolves every constructor parameter through the module, so there is no
338
+ argument list to pass. To hand the instance something of your own, declare it as a parameter and
339
+ intercept its id:
340
+
341
+ ```ts
342
+ interface SessionContext {
343
+ readonly player: Player;
344
+ }
345
+
346
+ @Injectable()
347
+ class Session {
348
+ constructor(
349
+ private economy: Economy,
350
+ private context: SessionContext,
351
+ ) {}
352
+ }
353
+
354
+ const session = module.createClassInstance(Session, {
355
+ overrideDependency: (info) => (info.id === Flamework.id<SessionContext>() ? { player } : undefined),
356
+ });
357
+ ```
358
+
359
+ Returning `undefined` falls back to the module's normal resolution, so you intercept only what you
360
+ mean to. This is how `@flamework-experimental/components` gives every component its `instance` and
361
+ `attributes`.
362
+
363
+ ## Realms
364
+
365
+ There is no `@Service` / `@Controller` split. A provider is not bound to a realm. The module that
366
+ registers it decides:
367
+
368
+ ```ts
369
+ // server entry point
370
+ .registerProviders("src/server/services")
371
+
372
+ // client entry point
373
+ .registerProviders("src/client/controllers")
374
+ ```
375
+
376
+ Shared providers go in a shared folder registered by both, or in a shared plugin included by both.
377
+
378
+ ## Patterns
379
+
380
+ **Interface plus alias for swappable implementations.** Declare the interface, register the concrete
381
+ class, and alias the interface to it. Tests register a different class under the same alias.
382
+
383
+ **A config provider at the top.** A function provider returning a frozen object is the simplest way
384
+ to get configuration into everything without a global.
385
+
386
+ **Factories over service locators.** If a provider needs to make many short-lived objects, inject
387
+ `Module` and use `createClassInstance` rather than passing the module around.
388
+
389
+ ## Caveats
390
+
391
+ - **Path registration takes every provider a file defines, exported or not.** Sometimes a
392
+ `@Provider()` class must stay out of the module that registers its folder, such as a fixture that
393
+ a test registers in a module of its own. Put that class in a folder no module registers, or inside
394
+ the function that uses it. A class declared inside a function is found only through its file's
395
+ exports.
396
+ - **A path is resolved in the project that compiles it.** The transformer turns it into a Rojo
397
+ path with that project's Rojo file. So a published package cannot register its own folders:
398
+ `registerProviders("src/...")` in a package gets a path in the package's project, which a game's
399
+ place does not have, and fails at runtime. In a package, register classes one by one with
400
+ `registerClassProvider`. See [Macros › Paths](07-macros.md#paths).
401
+ - **Path registration requires every ModuleScript in the folder**, so their import side effects
402
+ run. A ModuleScript that throws while loading fails the ignition with its path and error, as in v1.
403
+ Otherwise, a provider that silently failed to register would only show up later, as a missing
404
+ dependency. A registration whose own scope condition does not hold requires nothing.
405
+ - **Subclasses need their own decorator.** `class Fake extends Economy {}` without `@Provider()` is
406
+ not a provider; registering it explicitly raises, and path registration skips it.
407
+ - **`WaitForChild` yields.** If the folder has not replicated yet, ignition waits. A folder that
408
+ never comes (missing or misspelled, or empty in a fresh clone, which git leaves without it) is
409
+ warned about when you build, where the path is written, and at runtime after five seconds, by the
410
+ registration's name; the wait goes on. An empty folder that is there registers nothing.
411
+ - **Overlapping paths raise.** Registering `src/server` and `src/server/services` will hit
412
+ `provider ID was registered more than once`.
413
+ - **`@Injectable()` classes are not resolvable.** `resolveDependency<Session>()` will not find one.
414
+ That is the point of the decorator.
415
+ - **A missing dependency is a runtime error, not a compile error.** The exception is a component,
416
+ which the transformer refuses. `module could not resolve dependency 'X'` means the type was never
417
+ registered in this module or in anything it includes. For a class that has loaded, the message goes
418
+ on to say what the class is and what to do.
419
+ - **Constructor injection only.** There is no property or method injection.
420
+
421
+ ---
422
+
423
+ Previous: [Modules](02-modules.md) · Next: [Lifecycle events](04-lifecycle-events.md)