@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,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)