@flamework-experimental/core 2.0.0-alpha.3 → 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.
- package/README.md +11 -6
- package/docs/README.md +63 -0
- package/docs/guide/01-getting-started.md +334 -0
- package/docs/guide/02-modules.md +254 -0
- package/docs/guide/03-providers.md +423 -0
- package/docs/guide/04-lifecycle-events.md +423 -0
- package/docs/guide/05-components.md +793 -0
- package/docs/guide/06-networking.md +614 -0
- package/docs/guide/07-macros.md +332 -0
- package/docs/guide/08-plugins.md +203 -0
- package/docs/guide/09-project-structure.md +392 -0
- package/docs/guide/10-migrating-from-v1.md +573 -0
- package/docs/guide/11-scopes.md +165 -0
- package/docs/guide/12-testing.md +342 -0
- package/flamework.build +1 -1
- package/out/index.d.ts +1 -0
- package/out/init.luau +1 -0
- package/out/module/module.luau +1 -1
- package/out/module/moduleBuilder.luau +1 -1
- package/out/utility/getClassesInPath.d.ts +27 -1
- package/out/utility/getClassesInPath.luau +101 -19
- package/out/utility/pathRoot.d.ts +20 -1
- package/out/utility/pathRoot.luau +80 -4
- 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)
|