@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,573 @@
|
|
|
1
|
+
# 10. Migrating from v1
|
|
2
|
+
|
|
3
|
+
v2 is not a drop-in upgrade. The main change: v1's global container, which you never created
|
|
4
|
+
yourself, is now an explicit **module** that you build and ignite (start). Everything that used to
|
|
5
|
+
be built into that container is now a **plugin**.
|
|
6
|
+
|
|
7
|
+
## At a glance
|
|
8
|
+
|
|
9
|
+
| v1 | v2 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `@flamework/core`, `@flamework/components`, `@flamework/networking` | `@flamework-experimental/core`, `components`, `networking` (step 1) |
|
|
12
|
+
| `rbxts-transformer-flamework` | `@flamework-experimental/transformer` (step 1) |
|
|
13
|
+
| `@Service()` / `@Controller()` | `@Provider()` on both realms |
|
|
14
|
+
| `Flamework.addPaths("src/services")` | `.registerProviders("src/services")`; like v1, it finds the classes a module defines whether it exports them or not |
|
|
15
|
+
| `Flamework.addPaths(...)` to load a folder for what its modules do as they load | `requireModules("src/server/commands")`, built into core; nothing for a folder inside a registered folder, which registration already loads (step 8) |
|
|
16
|
+
| `Flamework.addPathsGlob("src/**/services")` | `.registerProvidersGlob("src/**/services")` / `ComponentPlugin.fromGlob(...)` |
|
|
17
|
+
| `@Optional()` / `includeOptionalClass` | `@Provider({ lazy: true })`, constructed when first resolved |
|
|
18
|
+
| `flamework.json` `profiling` | `flamework.config.json` `core.profiling`, or `createLifecyclePlugin({ profiling })` per module |
|
|
19
|
+
| Transformer options inline in `tsconfig.json` | the `transformer` section of `flamework.config.json`; the build refuses them on the entry (step 1) |
|
|
20
|
+
| Values sent as-is over remotes | unchanged by default; `networking.serialization` packs them into buffers with generated code |
|
|
21
|
+
| `OnInit` | unchanged |
|
|
22
|
+
| `Modding.createDecorator` / `getDecorators` | your own decorator + `@metadata reflect` + `Reflect`; see below |
|
|
23
|
+
| `Modding.getObjectFromId`, `Reflect.idToObj` | gone; there is no global registry |
|
|
24
|
+
| `Flamework.ignite()` | `Flamework.createModule()….ignite()` |
|
|
25
|
+
| Lifecycle events built in | still on: `LifecyclePlugin` is an ordinary plugin every module starts with; `disableDefaultLifecycle()` opts out |
|
|
26
|
+
| `Dependency<T>()` | resolves **registered providers** only, from the first module ignited or the one ignited with `{ default: true }`. `Dependency<T>(module)` answers from a given module. v1 built any decorated class on demand; a component or an unregistered class no longer works (see step 5) |
|
|
27
|
+
| `Flamework.resolveDependency(id)` | `Dependency<T>(undefined, id)`, or `module.resolveDependency<T>(id)` (step 5) |
|
|
28
|
+
| A `@Service()`/`@Controller()` class outside the added paths, a singleton once its module had loaded | `.registerClassProvider(C)` (step 12) |
|
|
29
|
+
| `Modding.createDependency(C)` | `module.createClassInstance(C)` with `@Injectable()` (step 12) |
|
|
30
|
+
| `Modding.createDeferredDependency(C)` | `module.createClassInstance(C)`; nothing hands out the object before its constructor has run |
|
|
31
|
+
| `Modding.resolveSingleton(C)` | `module.resolveDependency<C>()` or `Dependency<C>()`, for a registered provider (step 5) |
|
|
32
|
+
| `Modding.addListener(obj)` / `Modding.removeListener(obj)` | `module.listen<T>(obj)` for each interface, which returns the function that detaches it; or build it with `module.createClassInstance(C)` and detach it with `removeClassInstance` |
|
|
33
|
+
| `Modding.onListenerAdded<T>(cb)` | `target.observe<T>({ onAdded, onRemoved })` in a plugin; for components, `Components.onComponentAdded<T>(cb)` (step 7) |
|
|
34
|
+
| `Modding.Generic`, `Many`, `Caller<M>`, `TupleLabels`, … | `Modding.Target.*`, `Emit`, `Caller.*` (step 9) |
|
|
35
|
+
| Components auto-registered | `.includePlugin(ComponentPlugin.fromPath(…))` |
|
|
36
|
+
| `Components` injected globally | `Components` is provided by the component plugin; inject it as before |
|
|
37
|
+
| Tagged instances get their components in `Components.onStart` (`loadOrder: 0`), before most providers' `onStart` | after every provider's `onStart` (step 6) |
|
|
38
|
+
| An attribute changed to a value its guard rejects is ignored | the component is removed until it is valid, unless a `defaults` entry covers it (step 6) |
|
|
39
|
+
| `Flamework.implements` | unchanged |
|
|
40
|
+
| `Flamework.id`, `createGuard` | unchanged |
|
|
41
|
+
| `Networking.createEvent` | unchanged |
|
|
42
|
+
| Middleware `processNext(...)` returns a Promise | returns the value (step 10) |
|
|
43
|
+
| `connect` returns an `RBXScriptConnection`, tied through a BindableEvent to the script that connected | returns a `Networking.Connection`, not tied to that script's lifetime; nothing changes for a project that does not destroy its scripts (step 10) |
|
|
44
|
+
|
|
45
|
+
## Step by step
|
|
46
|
+
|
|
47
|
+
### 1. Swap the packages and the configuration
|
|
48
|
+
|
|
49
|
+
The packages moved to the `@flamework-experimental` scope, and the transformer moved in with them:
|
|
50
|
+
|
|
51
|
+
| v1 | v2 |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `@flamework/core` | `@flamework-experimental/core` |
|
|
54
|
+
| `@flamework/components` | `@flamework-experimental/components` |
|
|
55
|
+
| `@flamework/networking` | `@flamework-experimental/networking` |
|
|
56
|
+
| `rbxts-transformer-flamework` | `@flamework-experimental/transformer` |
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
npm uninstall @flamework/core @flamework/components @flamework/networking rbxts-transformer-flamework
|
|
60
|
+
npm install @flamework-experimental/core @flamework-experimental/components @flamework-experimental/networking
|
|
61
|
+
npm install -D @flamework-experimental/transformer
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Install the ones you use, and upgrade them together: the [changelog](https://github.com/Velover/ExperimentalFlameworkV2/blob/HEAD/CHANGELOG.md) says which
|
|
65
|
+
releases depend on each other. Then:
|
|
66
|
+
|
|
67
|
+
- **Imports.** `@flamework/core` becomes `@flamework-experimental/core`, and so on for every import.
|
|
68
|
+
- **`tsconfig.json`.** The transformer entry becomes
|
|
69
|
+
`{ "transform": "@flamework-experimental/transformer" }`, and `typeRoots` lists
|
|
70
|
+
`node_modules/@flamework-experimental` where it listed `node_modules/@flamework`. Move every
|
|
71
|
+
transformer option on the entry, such as `obfuscation`, `hashPrefix` or `idGenerationMode`, to the
|
|
72
|
+
`transformer` section of `flamework.config.json`. The build refuses them on the entry, and the
|
|
73
|
+
error names each one and the file to move it to. Any other key it names and tells you to remove,
|
|
74
|
+
such as v1's `preloadIds`, which has no counterpart.
|
|
75
|
+
- **`flamework.json`** becomes `flamework.config.json`, next to `tsconfig.json`, with a section per
|
|
76
|
+
package ([Project structure › Configuration](09-project-structure.md#configuration)). v1's
|
|
77
|
+
`profiling` is `core.profiling`. `logLevel` and `disableDependencyWarnings` have no counterpart
|
|
78
|
+
(v1's warning for `Dependency<T>()` before `ignite()` is now an error; see step 5), and the new
|
|
79
|
+
file rejects keys it does not know. Nothing reads `flamework.json` any more. If you have no
|
|
80
|
+
`flamework.config.json` yet and `tsconfig.json` is at the package root, the first build creates
|
|
81
|
+
one with just a `$schema` line, so your editor lists every option.
|
|
82
|
+
- **Rojo.** Where the project file maps `node_modules/@flamework`, map
|
|
83
|
+
`node_modules/@flamework-experimental` instead. That one line needs the transformer
|
|
84
|
+
2.0.0-alpha.5 or later, which maps itself to an empty Folder. With an older transformer, map each
|
|
85
|
+
runtime package by name, or the transformer's files reach the place. See
|
|
86
|
+
[Getting started › Rojo](01-getting-started.md#rojo).
|
|
87
|
+
- **Build output.** Delete `out/` before the first v2 build. The roblox-ts template builds
|
|
88
|
+
incrementally, with its `tsbuildinfo` in `out/`. An incremental build would start from v1's
|
|
89
|
+
`flamework.build`, which the transformer refuses: `Project was compiled on different version of
|
|
90
|
+
Flamework`, naming the tsbuildinfo to delete. The same happens after every later Flamework
|
|
91
|
+
upgrade while `incremental` is on; see
|
|
92
|
+
[Getting started › Incremental builds and upgrades](01-getting-started.md#incremental-builds-and-upgrades).
|
|
93
|
+
|
|
94
|
+
A library built on v1 imports `@flamework/core`, so it does not work with v2 until it is ported. For
|
|
95
|
+
example, `@rbxts/flamework-react-utils` calls `Flamework.resolveDependency`, whose replacement is in
|
|
96
|
+
step 5.
|
|
97
|
+
|
|
98
|
+
### 2. Replace the entry point
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// v1
|
|
102
|
+
import { Flamework } from "@flamework/core";
|
|
103
|
+
|
|
104
|
+
Flamework.addPaths("src/server/services");
|
|
105
|
+
Flamework.addPaths("src/server/components");
|
|
106
|
+
Flamework.ignite();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// v2
|
|
111
|
+
import { ComponentPlugin } from "@flamework-experimental/components";
|
|
112
|
+
import { Flamework } from "@flamework-experimental/core";
|
|
113
|
+
|
|
114
|
+
Flamework.createModule()
|
|
115
|
+
.includePlugin(ComponentPlugin.fromPath("src/server/components"))
|
|
116
|
+
.registerProviders("src/server/services")
|
|
117
|
+
.ignite();
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Components and providers are now registered separately. `registerProviders` only picks up
|
|
121
|
+
`@Provider()` classes, and `registerComponents` only picks up `@Component()` ones.
|
|
122
|
+
|
|
123
|
+
### 3. Rename the decorators
|
|
124
|
+
|
|
125
|
+
`@Service()` and `@Controller()` both become `@Provider()`. Nothing about a provider is
|
|
126
|
+
realm-specific any more. The realm that gets it is decided by which entry point registers its
|
|
127
|
+
folder, so keep the folders separate.
|
|
128
|
+
|
|
129
|
+
If you had a class used on both realms with different behaviour, that is now two classes in two
|
|
130
|
+
folders, or one class registered by both.
|
|
131
|
+
|
|
132
|
+
`loadOrder` moves to `@Provider({ loadOrder })`, with v1's meaning: lower runs first, the default is
|
|
133
|
+
`1`, and it orders `onInit` and `onStart`. One difference: v1 sorted by `loadOrder` before
|
|
134
|
+
dependencies, so a provider could be initialised before one it injected. v2 initialises dependencies
|
|
135
|
+
first, and uses `loadOrder` to order what that leaves free. `onStart` follows `loadOrder` alone. See
|
|
136
|
+
[Lifecycle events](04-lifecycle-events.md#load-order).
|
|
137
|
+
|
|
138
|
+
### 4. Lifecycle events are still on
|
|
139
|
+
|
|
140
|
+
`OnInit`, `OnStart`, `OnTick`, `OnPhysics` and `OnRender` work as they did. They are provided by
|
|
141
|
+
`LifecyclePlugin`, an ordinary plugin every module starts with, so there is nothing to add.
|
|
142
|
+
|
|
143
|
+
- `OnInit` still runs after construction, in dependency order. It may return a Promise, and
|
|
144
|
+
everything after it waits.
|
|
145
|
+
- `onPhysics` still receives `(dt, time)`.
|
|
146
|
+
- The signals are v1's too: `onTick` on `Heartbeat`, `onPhysics` on `PreSimulation` (v1 called it
|
|
147
|
+
`Stepped`), `onRender` on `PreRender`.
|
|
148
|
+
|
|
149
|
+
`OnRender` only connects on the client. A provider that implements it on the server does nothing.
|
|
150
|
+
|
|
151
|
+
### 5. `Dependency<T>()` still works -- for providers
|
|
152
|
+
|
|
153
|
+
It answers from the **default module**: the first module ignited in the realm. For a game, that is
|
|
154
|
+
the one the entry point ignites. For a provider there is nothing to change, though constructor
|
|
155
|
+
injection is still the better shape inside a provider:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
// still fine, once a module has ignited
|
|
159
|
+
const economy = Dependency<Economy>();
|
|
160
|
+
|
|
161
|
+
// better, inside a provider
|
|
162
|
+
constructor(private economy: Economy) {}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
If a realm ignites more than one module (tests, tools), pass `{ default: true }` to the one
|
|
166
|
+
`Dependency<T>()` should answer from, or use `module.resolveDependency<T>()` on the handle.
|
|
167
|
+
|
|
168
|
+
What changed is what it can resolve. v1's `Dependency<T>()` built **any** decorated class as a
|
|
169
|
+
singleton the first time it was asked for, registered or not: for example, a `@Component({})` with
|
|
170
|
+
no tag, reached only through `Dependency<T>()`. v2 resolves the providers a module registers, and
|
|
171
|
+
nothing else:
|
|
172
|
+
|
|
173
|
+
- A **component** is refused when you build (`'X' is a component (@Component), not a provider`).
|
|
174
|
+
Make it a `@Provider()` (`@Provider({ lazy: true })` keeps v1's "built when first asked for"), or
|
|
175
|
+
get it from `Components` on its instance.
|
|
176
|
+
- A `@Provider()` that no registered folder, registration, plugin or import brings in raises an
|
|
177
|
+
error at runtime. The error says so, and names the ModuleScript the class is defined in.
|
|
178
|
+
|
|
179
|
+
See [Providers](03-providers.md#asking-for-something-that-is-not-a-provider).
|
|
180
|
+
|
|
181
|
+
v1's `Flamework.resolveDependency(id)` took the id as a string, and libraries built on v1 call it:
|
|
182
|
+
`useFlameworkDependency` in `@rbxts/flamework-react-utils`, for one. In game code, a plain
|
|
183
|
+
`Dependency<T>()` is the whole replacement. Call it where you called the hook, in the component
|
|
184
|
+
body:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
// v1
|
|
188
|
+
const economy = useFlameworkDependency<Economy>();
|
|
189
|
+
|
|
190
|
+
// v2
|
|
191
|
+
const economy = Dependency<Economy>();
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
For a class provider, the usual kind, this is a cached lookup once the provider exists: it allocates
|
|
195
|
+
nothing, and every render gets the same object, so it needs no `useMemo`. A function provider is
|
|
196
|
+
different: its callback runs on every `Dependency` call, so each render gets what the callback
|
|
197
|
+
returns then.
|
|
198
|
+
|
|
199
|
+
The id form is for code that has the id as a string, such as a library. The id is `Dependency`'s
|
|
200
|
+
second argument: `Dependency<T>(undefined, id)` answers from the default module, and
|
|
201
|
+
`module.resolveDependency<T>(id)` from a module you hold. A macro of your own gets the id of a type
|
|
202
|
+
argument with `Modding.Target.Id<T>`:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { Dependency, Modding } from "@flamework-experimental/core";
|
|
206
|
+
|
|
207
|
+
/** @metadata macro */
|
|
208
|
+
export function resolve<T>(id?: Modding.Target.Id<T>): T {
|
|
209
|
+
return Dependency<T>(undefined, id);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const economy = resolve<Economy>();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
An id passed this way is not checked when you build, the way a type argument is. Asking for a
|
|
216
|
+
component's id raises an error when the call runs.
|
|
217
|
+
|
|
218
|
+
### 6. Update components
|
|
219
|
+
|
|
220
|
+
Register components through `ComponentPlugin`, and get `Components` by injection instead of
|
|
221
|
+
globally:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// v1
|
|
225
|
+
constructor(private components: Components) {} // worked because Components was a global service
|
|
226
|
+
|
|
227
|
+
// v2 -- the same code, but it works because ComponentPlugin provides Components
|
|
228
|
+
constructor(private components: Components) {}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The component API itself is largely unchanged. What is new:
|
|
232
|
+
|
|
233
|
+
- `ComponentMetadata` must be the first constructor parameter if a component declares its own
|
|
234
|
+
constructor.
|
|
235
|
+
- Component-to-component dependencies work: declare the other component as a parameter, and
|
|
236
|
+
Flamework waits for it.
|
|
237
|
+
- Attributes are writable again, as they were in v1: `this.attributes.speed = 32` writes back to the
|
|
238
|
+
instance. Alpha releases before this made them `Readonly`.
|
|
239
|
+
- An attribute or a child typed as an Instance or as a component becomes a
|
|
240
|
+
[link](05-components.md#links). Flamework resolves the `InstanceHandle`, waits for it, and exposes
|
|
241
|
+
the components through `childComponents` and `attributeComponents`. In v1 you resolved an Instance
|
|
242
|
+
attribute yourself.
|
|
243
|
+
- **An optional child is now rejected.** `BaseComponent<{}, Model & { Head?: BasePart }>` compiled in
|
|
244
|
+
v1, and left `this.instance.Head` raising whenever the child was absent, because Roblox errors on
|
|
245
|
+
indexing a child that does not exist. Instead, require the child, or drop it from the tree and use
|
|
246
|
+
`FindFirstChild`. A child typed as a component is rejected too: name a component that may or may
|
|
247
|
+
not be there with an optional [link attribute](05-components.md#instance-attributes), or look it
|
|
248
|
+
up with `getComponent`. Optional attributes are unaffected.
|
|
249
|
+
|
|
250
|
+
Two things behave differently:
|
|
251
|
+
|
|
252
|
+
- **Components attach after the providers start.** In v1, `Components` was itself a service and a
|
|
253
|
+
controller, with `loadOrder: 0`. It built the components of the instances tagged so far in its
|
|
254
|
+
own `onStart`, before the `onStart` of every provider left at the default `loadOrder`. v2 starts
|
|
255
|
+
watching tags once the module has ignited: after every provider's `onStart` has been called
|
|
256
|
+
(whatever its `loadOrder`) and has run up to its first yield. So a provider's `onStart` that reads
|
|
257
|
+
`getAllComponents<T>()` finds none of the instances tagged before ignition. Connect
|
|
258
|
+
`onComponentAdded<T>(cb)` there instead: it hears about each of them as it is built. (`onInit`
|
|
259
|
+
saw none in v1 either.)
|
|
260
|
+
- **An attribute its guard rejects takes the component down.** v1 ignored such a change, and the
|
|
261
|
+
component kept the last good value. v2 removes the component. Once the attribute is valid again,
|
|
262
|
+
v2 builds the component again, reading the attributes afresh. The exception is an attribute that a
|
|
263
|
+
`defaults` entry covers: the component stays, with its last good value, as in v1.
|
|
264
|
+
`refreshAttributes: false` does not change this. It stops `this.attributes` following the
|
|
265
|
+
instance and stops `onAttributeChanged` firing, but it does not stop the check. See
|
|
266
|
+
[What takes a component down again](05-components.md#what-takes-a-component-down-again).
|
|
267
|
+
|
|
268
|
+
### 7. Replace `Modding.onListenerAdded`
|
|
269
|
+
|
|
270
|
+
v1's `Modding.onListenerAdded<T>(cb)` worked from anywhere, at any time. It replayed the providers
|
|
271
|
+
and components implementing `T` that already existed, then reported new ones. v2 has no global
|
|
272
|
+
registry to ask. What replaces it depends on what you listen for.
|
|
273
|
+
|
|
274
|
+
**Providers: observe from a plugin.** A plugin's `target.observe<T>` hears about every object in the
|
|
275
|
+
module that implements `T`: each provider as it is constructed, and the components and anything
|
|
276
|
+
else attached through `createClassInstance` or `listen`. It hears about each one again when it
|
|
277
|
+
goes:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
// v1
|
|
281
|
+
Modding.onListenerAdded<OnPlayerJoined>((listener) => listeners.add(listener));
|
|
282
|
+
Modding.onListenerRemoved<OnPlayerJoined>((listener) => listeners.delete(listener));
|
|
283
|
+
|
|
284
|
+
// v2
|
|
285
|
+
Flamework.createPlugin("PlayerListeners", (target) => {
|
|
286
|
+
target.observe<OnPlayerJoined>({
|
|
287
|
+
onAdded: (value) => listeners.add(value),
|
|
288
|
+
onRemoved: (value) => listeners.delete(value),
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`observe` replays nothing, so call it in the plugin's setup, before the first provider is
|
|
294
|
+
constructed. Only a plugin has it: a provider cannot subscribe to the module it is in. A provider
|
|
295
|
+
that subscribed from its `onStart` in v1 now takes the set from the plugin instead. The plugin keeps
|
|
296
|
+
the set and hands it to the module with `provideInstance`:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
export class PlayerListeners {
|
|
300
|
+
public readonly all = new Set<OnPlayerJoined>();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export const PlayerListenersPlugin = Flamework.createPlugin("PlayerListeners", (target) => {
|
|
304
|
+
const listeners = new PlayerListeners();
|
|
305
|
+
target.provideInstance(listeners);
|
|
306
|
+
target.observe<OnPlayerJoined>({
|
|
307
|
+
onAdded: (value) => listeners.all.add(value),
|
|
308
|
+
onRemoved: (value) => listeners.all.delete(value),
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
@Provider()
|
|
313
|
+
export class Lobby implements OnStart {
|
|
314
|
+
constructor(private readonly listeners: PlayerListeners) {}
|
|
315
|
+
|
|
316
|
+
public onStart() {
|
|
317
|
+
Players.PlayerAdded.Connect((player) => {
|
|
318
|
+
for (const listener of this.listeners.all) listener.onPlayerJoined(player);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Components: ask `Components`.** Its polymorphic methods take an interface and answer at any time.
|
|
325
|
+
`getAllComponents<T>()` gives the components that exist now. `onComponentAdded<T>(cb)` and
|
|
326
|
+
`onComponentRemoved<T>(cb)` report the ones that come and go. Unlike v1's `onListenerAdded`,
|
|
327
|
+
`onComponentAdded` does not replay the ones that already exist.
|
|
328
|
+
|
|
329
|
+
In an eager provider's `onStart`, subscribing is enough. Tagged instances get their components
|
|
330
|
+
after every provider's `onStart` (step 6), so no tagged instance has its component yet, and
|
|
331
|
+
`onComponentAdded` hears about each one as it is built:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
@Provider()
|
|
335
|
+
export class PriceTags implements OnStart {
|
|
336
|
+
constructor(private readonly components: Components) {}
|
|
337
|
+
|
|
338
|
+
public onStart() {
|
|
339
|
+
this.components.onComponentAdded<ShowsPrice>((tag) => this.show(tag));
|
|
340
|
+
this.components.onComponentRemoved<ShowsPrice>((tag) => this.hide(tag));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private show(tag: ShowsPrice) {}
|
|
344
|
+
private hide(tag: ShowsPrice) {}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Read the existing ones first when you subscribe after ignition: from a `@Provider({ lazy: true })`
|
|
349
|
+
first resolved later, after a yield in `onStart`, or from an event handler. By then components
|
|
350
|
+
exist, and only `getAllComponents<T>()` gives them:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
for (const tag of this.components.getAllComponents<ShowsPrice>()) this.show(tag);
|
|
354
|
+
this.components.onComponentAdded<ShowsPrice>((tag) => this.show(tag));
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
A generic helper of your own (an `onListenerAdded<T>` kept for the old call sites, say) is a macro.
|
|
358
|
+
It takes `id?: Modding.Target.Id<T>` and passes it on as the last argument:
|
|
359
|
+
`getAllComponents<T>(id)`, `onComponentAdded<T>(cb, id)`, `onComponentRemoved<T>(cb, id)`.
|
|
360
|
+
|
|
361
|
+
See [Plugins](08-plugins.md#observing-interfaces) and
|
|
362
|
+
[Working with components](05-components.md#working-with-components).
|
|
363
|
+
|
|
364
|
+
### 8. Custom decorators
|
|
365
|
+
|
|
366
|
+
v1's `Modding.createDecorator`, `createMetaDecorator`, `getDecorators`, `getDecorator`,
|
|
367
|
+
`getPropertyDecorators` and `Reflect.decorate` are gone, along with the global class registry behind
|
|
368
|
+
them (`Reflect.idToObj`, `Modding.getObjectFromId`). A decorator is now an ordinary function. Its
|
|
369
|
+
JSDoc tells the transformer which metadata to attach, and the function records whatever it wants on
|
|
370
|
+
the class with `Reflect`:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
import { Reflect } from "@flamework-experimental/core";
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @metadata reflect identifier flamework:implements
|
|
377
|
+
*/
|
|
378
|
+
export function Command(name: string) {
|
|
379
|
+
return (ctor: object) => {
|
|
380
|
+
Reflect.defineMetadata(ctor, "myGame:command", name);
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Finding decorated classes works by path, exactly like providers. Where v1 offered
|
|
386
|
+
`Modding.getDecorators<typeof Command>()`, walk a folder and filter on your own metadata.
|
|
387
|
+
`getClassesInPath` returns the classes v1's registry held, limited to one folder. That is every
|
|
388
|
+
class with its own Flamework identifier that the ModuleScripts under the path define at their top
|
|
389
|
+
level, exported or not, plus anything they export that carries one. Each is returned once:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
import { getClassesInPath, Reflect } from "@flamework-experimental/core";
|
|
393
|
+
|
|
394
|
+
export function findCommands(path: readonly string[], register: (name: string, ctor: object) => void) {
|
|
395
|
+
for (const ctor of getClassesInPath(path)) {
|
|
396
|
+
const name = Reflect.getOwnMetadata<string>(ctor, "myGame:command");
|
|
397
|
+
if (name !== undefined) register(name, ctor);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
`getClassesInPath` takes the Rojo path array that the `path` intrinsic produces. Wrap it in a macro
|
|
403
|
+
of your own so callers can pass `"src/server/commands"`; [Macros › Paths](07-macros.md#paths) shows
|
|
404
|
+
one.
|
|
405
|
+
|
|
406
|
+
Some folders were loaded in v1 with `Flamework.addPaths` only for what their ModuleScripts do as
|
|
407
|
+
they load: modules that register themselves with a library, say. Load those with
|
|
408
|
+
`requireModules("src/server/commands")`, which core has built in; see
|
|
409
|
+
[Macros › Paths](07-macros.md#paths). A folder inside a folder you register needs nothing:
|
|
410
|
+
registration already requires every ModuleScript under it (see
|
|
411
|
+
[Providers › How it actually works](03-providers.md#how-it-actually-works)). That was true in v1 too:
|
|
412
|
+
once `addPaths` had loaded the outer folder, an `addPaths` of a folder inside it did nothing.
|
|
413
|
+
|
|
414
|
+
Property and method decorators work the same way, with
|
|
415
|
+
`Reflect.defineMetadata(ctor, key, value, propertyName)`.
|
|
416
|
+
|
|
417
|
+
### 9. Rename the macro types
|
|
418
|
+
|
|
419
|
+
The types that macro parameters use are grouped now. Types that describe the callsite are under
|
|
420
|
+
`Modding.Caller`, types that describe a type argument are under `Modding.Target`, and `Many` is now
|
|
421
|
+
`Emit`. The rename is mechanical, except where the table says what else changed:
|
|
422
|
+
|
|
423
|
+
| v1 | v2 |
|
|
424
|
+
|---|---|
|
|
425
|
+
| `Modding.Many<T>` | `Modding.Emit<T>` |
|
|
426
|
+
| `Modding.Generic<T, "id">` | `Modding.Target.Id<T>` |
|
|
427
|
+
| `Modding.Generic<T, "text">` | `Modding.Target.Text<T>` |
|
|
428
|
+
| `Modding.Generic<T, "guard">` | `Modding.Target.Guard<T>` |
|
|
429
|
+
| `Modding.GenericMany<T, "id" \| "guard">` | `Modding.Emit<{ id: Modding.Target.Id<T>; guard: Modding.Target.Guard<T> }>` |
|
|
430
|
+
| `Modding.Caller<"line">`, and `"character"`, `"width"`, `"text"` | `Modding.Caller.Line`, and `Character`, `Width`, `Text` |
|
|
431
|
+
| `Modding.Caller<"uuid">` | `Modding.Caller.Uuid`. v1 generated a random one on every compile; v2 derives it from the callsite, so the same source gives the same one in every build unless obfuscation is on |
|
|
432
|
+
| `Modding.CallerMany<"line" \| "text">` | `Modding.Emit<{ line: Modding.Caller.Line; text: Modding.Caller.Text }>` |
|
|
433
|
+
| `Modding.TupleLabels<T>` | `Modding.Target.Labels<T>` |
|
|
434
|
+
| `Modding.Hash<T, C>`, `Modding.Obfuscate<T, C>` | `Modding.Target.Hash<T, C>`, `Modding.Target.Obfuscate<T, C>` |
|
|
435
|
+
| `Modding.Intrinsic<"path", [T]>` | `Modding.Intrinsic<"path", [T], string[]>`: the value is one Rojo path, where v1's was a list holding one (`string[][]`). See [Macros › Paths](07-macros.md#paths) |
|
|
436
|
+
| `IntrinsicSymbolId<T>` from `@flamework/core/out/utility` (`Modding.Intrinsic<"symbol-id", [T], string>`) | `Modding.Target.Id<T>` |
|
|
437
|
+
| `Modding.Intrinsic<"declaration-uid", [], string>`, the id of the declaration a call sits in | gone; `Modding.Caller.Uuid` identifies the callsite |
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
// v1
|
|
441
|
+
/** @metadata macro */
|
|
442
|
+
export function validate<T>(value: unknown, guard?: Modding.Generic<T, "guard">): value is T {
|
|
443
|
+
return guard!(value);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// v2
|
|
447
|
+
/** @metadata macro */
|
|
448
|
+
export function validate<T>(value: unknown, guard?: Modding.Target.Guard<T>): value is T {
|
|
449
|
+
return guard!(value);
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
New in v2:
|
|
454
|
+
|
|
455
|
+
- `Modding.Caller.Constant<T>`: metadata generated once per callsite and shared by every call.
|
|
456
|
+
- `Modding.Target.Dependency<T>` and `DependencyConcise<T>`: the id and metadata that dependency
|
|
457
|
+
injection resolves a type by.
|
|
458
|
+
|
|
459
|
+
See [Macros](07-macros.md).
|
|
460
|
+
|
|
461
|
+
### 10. Networking
|
|
462
|
+
|
|
463
|
+
`createEvent`, `createFunction`, namespaces, guards and `Networking.Skip` are as they were. What
|
|
464
|
+
changed:
|
|
465
|
+
|
|
466
|
+
- **`processNext` returns the next link's result, not a Promise.** For an event that is nothing;
|
|
467
|
+
for a function it is the value or `Networking.Skip`. A middleware that returns `processNext(...)`,
|
|
468
|
+
or `await`s it, needs no change. One that chained on it, with `processNext(...).andThen(f)` (or
|
|
469
|
+
`.then(f)`), now calls `f` on the result instead:
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
// v1
|
|
473
|
+
const logPurchases: Networking.FunctionMiddleware<[itemId: string], boolean> = (processNext) => {
|
|
474
|
+
return (player, itemId) =>
|
|
475
|
+
processNext(player, itemId).andThen((bought) => {
|
|
476
|
+
print(player, itemId, bought);
|
|
477
|
+
return bought;
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// v2
|
|
482
|
+
const logPurchases: Networking.FunctionMiddleware<[itemId: string], boolean> = (processNext) => {
|
|
483
|
+
return (player, itemId) => {
|
|
484
|
+
const bought = processNext(player, itemId);
|
|
485
|
+
print(player, itemId, bought);
|
|
486
|
+
return bought;
|
|
487
|
+
};
|
|
488
|
+
};
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
An error further down the chain is now raised through `processNext`, instead of rejecting a
|
|
492
|
+
Promise. So a `.catch` or `.finally` becomes a `try`/`catch` or `try`/`finally` around the call.
|
|
493
|
+
See [Middleware](06-networking.md#middleware).
|
|
494
|
+
- **`connect` and `registerHandler` return a `Networking.Connection`**, networking's own type, not
|
|
495
|
+
an engine `RBXScriptConnection`. It has `Connected`, `Disconnect()`, and `Destroy()` for maids and
|
|
496
|
+
janitors. Code that stores one as `RBXScriptConnection` still compiles, since the shape matches,
|
|
497
|
+
but name `Networking.Connection` instead. `typeIs(connection, "RBXScriptConnection")` is false for
|
|
498
|
+
one.
|
|
499
|
+
- **Handlers are no longer tied to the lifetime of the script that connected them**, as v1's were
|
|
500
|
+
through a BindableEvent. A roblox-ts project does not destroy its scripts, so nothing changes for
|
|
501
|
+
a normal project.
|
|
502
|
+
- **Each handler runs at once, on a thread of its own**, newest connection first. In v1 it ran when
|
|
503
|
+
a BindableEvent delivered it, which the engine defers under `SignalBehavior.Deferred`.
|
|
504
|
+
|
|
505
|
+
### 11. Removed without replacement
|
|
506
|
+
|
|
507
|
+
- **Primitive dependencies.** v1 could inject a string or number literal type (`$ps:`/`$pn:` ids).
|
|
508
|
+
Register a function provider under an interface instead.
|
|
509
|
+
- **`Modding.registerDependency`.** Use a function or alias provider on the module.
|
|
510
|
+
- **`Modding.onListenerAdded` without an id** (every listener). Register an interface per event.
|
|
511
|
+
- **`Flamework.hash`.** `Modding.Target.Hash` still exists for writing a macro of your own.
|
|
512
|
+
|
|
513
|
+
### 12. Externally created classes
|
|
514
|
+
|
|
515
|
+
v1 made a `@Service()` or `@Controller()` class a singleton as soon as its ModuleScript had loaded,
|
|
516
|
+
wherever it was. v2 registers only what the module is given. So a provider that is in no registered
|
|
517
|
+
folder has to be registered by hand:
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
// v1
|
|
521
|
+
Modding.createDependency(Helper); // a one-off instance, with injection
|
|
522
|
+
|
|
523
|
+
// v2 -- a provider outside the registered folders
|
|
524
|
+
.registerClassProvider(SomeService)
|
|
525
|
+
|
|
526
|
+
// v2 -- a one-off instance with injection but no registration
|
|
527
|
+
@Injectable()
|
|
528
|
+
class Helper {}
|
|
529
|
+
|
|
530
|
+
module.createClassInstance(Helper);
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## What did not change
|
|
534
|
+
|
|
535
|
+
The transformer works the same way for everything built on it: `Flamework.id`,
|
|
536
|
+
`Flamework.implements`, `Flamework.createGuard`, `Modding.inspect`, and writing your own macros with
|
|
537
|
+
`@metadata macro`. The macro types were renamed, though ([step 9](#9-rename-the-macro-types)).
|
|
538
|
+
|
|
539
|
+
Networking's `createEvent`, `createFunction`, namespaces, guards, `Networking.Skip` and the error
|
|
540
|
+
values are as they were. Middleware, connections and handlers changed ([step 10](#10-networking)).
|
|
541
|
+
|
|
542
|
+
## Things to check after migrating
|
|
543
|
+
|
|
544
|
+
- Did you call `disableDefaultLifecycle()` anywhere? It is now the only way to lose lifecycle events
|
|
545
|
+
(`onInit` included), and components stop ticking with them.
|
|
546
|
+
- Did anything rely on `@Optional`? Replace it with `@Provider({ lazy: true })`.
|
|
547
|
+
- Did anything rely on `Modding.getDecorators`? Replace it with path scanning and your own metadata.
|
|
548
|
+
- Are your component folders registered with `ComponentPlugin`, not `registerProviders`?
|
|
549
|
+
- Did any `@Service` rely on being server-only? Providers are not tied to a realm; the module that
|
|
550
|
+
registers them decides.
|
|
551
|
+
- Did anything rely on `loadOrder`? Move it to `@Provider({ loadOrder })`. A provider it injects is
|
|
552
|
+
now initialised before it, whatever the numbers say.
|
|
553
|
+
- Does anything call `Dependency<T>()` on a component, or on a class no folder registers? v1 built
|
|
554
|
+
it on demand; v2 does not.
|
|
555
|
+
- Is any decorated class in a registered folder meant to stay out of the module? Unexported classes
|
|
556
|
+
are registered now, as in v1: move it out of the folder, or into the function that uses it.
|
|
557
|
+
- Does a provider's `onStart` expect the components of tagged instances to exist already? They are
|
|
558
|
+
built after it now.
|
|
559
|
+
- Does anything count on a component surviving an invalid attribute? Give the attribute a
|
|
560
|
+
`defaults` entry.
|
|
561
|
+
- Did anything count on a networking handler going away with the script that connected it?
|
|
562
|
+
Handlers are no longer tied to that script's lifetime; a roblox-ts project does not destroy its
|
|
563
|
+
scripts, so nothing changes for a normal project.
|
|
564
|
+
- Is the whole `node_modules/@flamework-experimental` folder mapped in your Rojo project, with a
|
|
565
|
+
transformer older than 2.0.0-alpha.5? Map the runtime packages one by one, or update the
|
|
566
|
+
transformer.
|
|
567
|
+
- Do any constructors yield? v1 tolerated that; now it stalls ignition.
|
|
568
|
+
- Is there exactly one `ignite()` per realm? Two containers do not share providers, and
|
|
569
|
+
`Dependency<T>()` answers from the first.
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
Previous: [Project structure](09-project-structure.md) · Back to the [index](../README.md)
|